Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions xrspatial/geotiff/_backends/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,13 @@ def read_geotiff_gpu(source: str, *,
stable_only : bool, default False
[experimental] Read-side opt-in for stable-tier sources only.
The GPU read path does not consume VRT sources directly (VRT
routing happens in ``open_geotiff``), so this kwarg is accepted
for cross-backend signature symmetry and is a no-op on the GPU
eager / chunked paths. See ``open_geotiff`` for the full
description.
routing happens in ``open_geotiff``), but it does route HTTP /
fsspec sources through the CPU fallback, and those advanced-tier
readers must be gated. With ``stable_only=True`` a remote source
raises ``RemoteStableSourcesOnlyError`` before any cupy import or
decode, matching ``open_geotiff`` and ``read_geotiff_dask``. Pass
``allow_experimental_codecs=True`` to unlock the advanced tier.
See ``open_geotiff`` for the full description.
allow_experimental_codecs : bool, default False
[experimental] Read-side opt-in for Tier 3 experimental codecs
(``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``). The GPU read path
Expand Down Expand Up @@ -275,6 +278,21 @@ def read_geotiff_gpu(source: str, *,
max_cloud_bytes=max_cloud_bytes,
)

# ``open_geotiff`` and ``read_geotiff_dask`` gate ``stable_only=True``
# for advanced-tier HTTP / fsspec sources before dispatching. This GPU
# entry point is also a direct public reader and it routes remote
# sources through the CPU fallback below, so a direct caller could read
# an advanced-tier remote source under ``stable_only=True`` unless the
# same gate fires here. Run it before the cupy import, the CUDA
# preflight, and the remote fallback so the rejection is independent of
# GPU availability, matching the other two readers.
from .._validation import _validate_stable_only_remote
_validate_stable_only_remote(
source,
stable_only=stable_only,
allow_experimental_codecs=allow_experimental_codecs,
)

new_passed = on_gpu_failure is not _ON_GPU_FAILURE_SENTINEL
old_passed = gpu is not _GPU_DEPRECATED_SENTINEL
if new_passed and old_passed:
Expand Down
49 changes: 48 additions & 1 deletion xrspatial/geotiff/tests/test_stable_only_remote_2821.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,24 @@
import pytest

from xrspatial.geotiff import (GeoTIFFAmbiguousMetadataError, RemoteStableSourcesOnlyError,
open_geotiff, read_geotiff_dask)
open_geotiff, read_geotiff_dask, read_geotiff_gpu)
from xrspatial.geotiff.tests._helpers.tiff_builders import make_minimal_tiff

fsspec = pytest.importorskip("fsspec")


def _gpu_available() -> bool:
try:
import cupy
return bool(cupy.cuda.is_available())
except Exception:
return False


_HAS_GPU = _gpu_available()
_gpu_only = pytest.mark.skipif(not _HAS_GPU, reason="cupy + CUDA required")


_MEMORY_URL = "memory:///stable_only_2821/sample.tif"


Expand Down Expand Up @@ -134,3 +146,38 @@ def test_local_dask_source_succeeds_under_stable_only(local_tiff_path):
"""A plain local-file dask read still works under ``stable_only=True``."""
result = open_geotiff(local_tiff_path, stable_only=True, chunks=2)
assert result.shape == (4, 4)


# ---------------------------------------------------------------------------
# Direct GPU entry point (issue #2867)
#
# ``read_geotiff_gpu`` is a public direct reader that routes HTTP / fsspec
# sources through the CPU fallback, so it must apply the same remote gate as
# ``open_geotiff`` and ``read_geotiff_dask``. The gate runs before the cupy
# import and the CUDA preflight, so the rejection tests run on a CPU-only
# machine -- no GPU required. Only the unlock test, which performs a real
# read, needs cupy + CUDA.
# ---------------------------------------------------------------------------


def test_gpu_direct_fsspec_source_rejected_under_stable_only(memory_tiff_url):
"""The direct ``read_geotiff_gpu`` entry point gates remote sources too."""
with pytest.raises(RemoteStableSourcesOnlyError) as excinfo:
read_geotiff_gpu(memory_tiff_url, stable_only=True)
_assert_remote_stable_error(excinfo)


def test_gpu_direct_http_source_rejected_under_stable_only():
"""An ``http://`` source is gated on the GPU path before any fetch."""
with pytest.raises(RemoteStableSourcesOnlyError) as excinfo:
read_geotiff_gpu("http://example.invalid/sample.tif", stable_only=True)
_assert_remote_stable_error(excinfo)


@_gpu_only
def test_gpu_direct_fsspec_source_allowed_with_experimental_optin(memory_tiff_url):
"""``allow_experimental_codecs=True`` unlocks the advanced tier on GPU."""
result = read_geotiff_gpu(
memory_tiff_url, stable_only=True, allow_experimental_codecs=True,
)
assert result.shape == (4, 4)
Loading