Skip to content

Commit 3e2b35e

Browse files
authored
Fix multi_stop_search on cupy and dask+cupy backends (#3637)
* Handle cupy and dask+cupy segments in multi_stop_search (#3630) Segment extraction assumed .get() meant cupy and everything else was safe for np.asarray(seg.values), which crashes on dask-of-cupy results (implicit cupy->numpy conversion). Route all backends through a _segment_to_numpy helper (compute dask, then .get() cupy) in both the stitching loop and _optimize_waypoint_order, and convert the stitched output back to the input's array type so cupy and dask inputs no longer come back numpy-backed. Fixes the no-op conversion expression in test_multi_stop_cupy_matches_numpy and adds a dask+cupy test. * Document output array-type preservation in multi_stop_search (#3630)
1 parent 503bad8 commit 3e2b35e

2 files changed

Lines changed: 69 additions & 12 deletions

File tree

xrspatial/pathfinding.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,20 @@ def _tour_cost(t):
13061306
return tour, _tour_cost(tour)
13071307

13081308

1309+
def _segment_to_numpy(seg_data):
1310+
"""Materialize an a_star_search segment result as a numpy array.
1311+
1312+
Handles all four backends: numpy passes through, cupy uses
1313+
``.get()``, dask computes first (yielding cupy blocks for
1314+
dask+cupy, which then also need ``.get()``).
1315+
"""
1316+
if da is not None and isinstance(seg_data, da.Array):
1317+
seg_data = seg_data.compute()
1318+
if hasattr(seg_data, 'get'): # cupy -> numpy
1319+
seg_data = seg_data.get()
1320+
return np.asarray(seg_data)
1321+
1322+
13091323
def _optimize_waypoint_order(surface, waypoints, barriers, x, y,
13101324
connectivity, snap, friction, search_radius):
13111325
"""Build pairwise cost matrix and solve TSP with fixed endpoints.
@@ -1328,11 +1342,7 @@ def _optimize_waypoint_order(surface, waypoints, barriers, x, y,
13281342
snap_start=snap, snap_goal=snap,
13291343
friction=friction, search_radius=search_radius,
13301344
)
1331-
seg_data = seg.data
1332-
if hasattr(seg_data, 'get'):
1333-
seg_vals = seg_data.get()
1334-
else:
1335-
seg_vals = np.asarray(seg.values)
1345+
seg_vals = _segment_to_numpy(seg.data)
13361346
goal_py, goal_px = _get_pixel_id(waypoints[j], surface, x, y)
13371347
goal_cost = seg_vals[goal_py, goal_px]
13381348
if np.isfinite(goal_cost):
@@ -1395,7 +1405,8 @@ def multi_stop_search(surface: xr.DataArray,
13951405
Returns
13961406
-------
13971407
xr.DataArray or xr.Dataset
1398-
Cumulative path cost surface. Attributes include
1408+
Cumulative path cost surface, backed by the same array type as
1409+
*surface* (numpy, cupy, dask, or dask+cupy). Attributes include
13991410
``waypoint_order``, ``segment_costs``, and ``total_cost``.
14001411
A Dataset input returns a Dataset of per-variable results.
14011412
@@ -1474,11 +1485,7 @@ def multi_stop_search(surface: xr.DataArray,
14741485
snap_start=snap, snap_goal=snap,
14751486
friction=friction, search_radius=search_radius,
14761487
)
1477-
seg_data = seg.data
1478-
if hasattr(seg_data, 'get'):
1479-
seg_vals = seg_data.get() # cupy -> numpy
1480-
else:
1481-
seg_vals = np.asarray(seg.values)
1488+
seg_vals = _segment_to_numpy(seg.data)
14821489

14831490
goal_py, goal_px = waypoint_pixels[i + 1]
14841491

@@ -1508,6 +1515,17 @@ def multi_stop_search(surface: xr.DataArray,
15081515
segment_costs.append(float(seg_goal_cost))
15091516
cumulative_cost += seg_goal_cost
15101517

1518+
# Match the input's array type, like a_star_search does
1519+
if _is_dask:
1520+
chunks = surface_data.chunks
1521+
path_data = da.from_array(path_data, chunks=chunks)
1522+
if has_cuda_and_cupy() and is_dask_cupy(surface):
1523+
import cupy
1524+
path_data = path_data.map_blocks(cupy.asarray)
1525+
elif has_cuda_and_cupy() and is_cupy_array(surface_data):
1526+
import cupy
1527+
path_data = cupy.asarray(path_data)
1528+
15111529
path_agg = xr.DataArray(
15121530
path_data,
15131531
coords=surface.coords,

xrspatial/tests/test_pathfinding.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,9 @@ def test_multi_stop_dask_matches_numpy():
10781078
path_np = multi_stop_search(agg_np, [wp0, wp1, wp2])
10791079
path_dask = multi_stop_search(agg_dask, [wp0, wp1, wp2])
10801080

1081+
# output should stay dask-backed, matching a_star_search
1082+
assert isinstance(path_dask.data, da.Array)
1083+
10811084
np.testing.assert_allclose(
10821085
np.asarray(path_dask.values),
10831086
path_np.values,
@@ -1099,13 +1102,49 @@ def test_multi_stop_cupy_matches_numpy():
10991102
path_np = multi_stop_search(agg_np, [wp0, wp1, wp2])
11001103
path_cupy = multi_stop_search(agg_cupy, [wp0, wp1, wp2])
11011104

1105+
# output should stay cupy-backed, matching a_star_search
1106+
import cupy
1107+
assert isinstance(path_cupy.data, cupy.ndarray)
1108+
11021109
np.testing.assert_allclose(
1103-
path_cupy.data if not hasattr(path_cupy.data, 'get') else path_cupy.data,
1110+
path_cupy.data.get(),
11041111
path_np.values,
11051112
equal_nan=True, atol=1e-10,
11061113
)
11071114

11081115

1116+
@cuda_and_cupy_available
1117+
@pytest.mark.skipif(not has_dask_array(), reason="Requires dask.Array")
1118+
def test_multi_stop_dask_cupy_matches_numpy():
1119+
"""Dask+CuPy multi-stop should not crash and should match numpy."""
1120+
data = np.ones((8, 8))
1121+
wp0 = (7.0, 0.0)
1122+
wp1 = (4.0, 3.0)
1123+
wp2 = (0.0, 7.0)
1124+
1125+
agg_np = _make_raster(data, backend='numpy')
1126+
agg_dc = _make_raster(data, backend='dask+cupy', chunks=(4, 4))
1127+
1128+
path_np = multi_stop_search(agg_np, [wp0, wp1, wp2])
1129+
path_dc = multi_stop_search(agg_dc, [wp0, wp1, wp2])
1130+
1131+
# output should stay dask-backed with cupy blocks
1132+
assert isinstance(path_dc.data, da.Array)
1133+
computed = path_dc.data.compute()
1134+
assert hasattr(computed, 'get') # cupy blocks
1135+
np.testing.assert_allclose(
1136+
computed.get(),
1137+
path_np.values,
1138+
equal_nan=True, atol=1e-10,
1139+
)
1140+
1141+
# optimize_order goes through the same extraction path
1142+
path_dc_opt = multi_stop_search(
1143+
agg_dc, [wp0, wp1, wp2], optimize_order=True)
1144+
computed_opt = path_dc_opt.data.compute()
1145+
assert np.isfinite(computed_opt.get()).any()
1146+
1147+
11091148
# =====================================================================
11101149
# Issue #1439: input validation
11111150
# =====================================================================

0 commit comments

Comments
 (0)