Skip to content

Commit c5c3386

Browse files
committed
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.
1 parent b9ba8a8 commit c5c3386

2 files changed

Lines changed: 67 additions & 11 deletions

File tree

xrspatial/pathfinding.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,20 @@ def _tour_cost(t):
12841284
return tour, _tour_cost(tour)
12851285

12861286

1287+
def _segment_to_numpy(seg_data):
1288+
"""Materialize an a_star_search segment result as a numpy array.
1289+
1290+
Handles all four backends: numpy passes through, cupy uses
1291+
``.get()``, dask computes first (yielding cupy blocks for
1292+
dask+cupy, which then also need ``.get()``).
1293+
"""
1294+
if da is not None and isinstance(seg_data, da.Array):
1295+
seg_data = seg_data.compute()
1296+
if hasattr(seg_data, 'get'): # cupy -> numpy
1297+
seg_data = seg_data.get()
1298+
return np.asarray(seg_data)
1299+
1300+
12871301
def _optimize_waypoint_order(surface, waypoints, barriers, x, y,
12881302
connectivity, snap, friction, search_radius):
12891303
"""Build pairwise cost matrix and solve TSP with fixed endpoints.
@@ -1306,11 +1320,7 @@ def _optimize_waypoint_order(surface, waypoints, barriers, x, y,
13061320
snap_start=snap, snap_goal=snap,
13071321
friction=friction, search_radius=search_radius,
13081322
)
1309-
seg_data = seg.data
1310-
if hasattr(seg_data, 'get'):
1311-
seg_vals = seg_data.get()
1312-
else:
1313-
seg_vals = np.asarray(seg.values)
1323+
seg_vals = _segment_to_numpy(seg.data)
13141324
goal_py, goal_px = _get_pixel_id(waypoints[j], surface, x, y)
13151325
goal_cost = seg_vals[goal_py, goal_px]
13161326
if np.isfinite(goal_cost):
@@ -1452,11 +1462,7 @@ def multi_stop_search(surface: xr.DataArray,
14521462
snap_start=snap, snap_goal=snap,
14531463
friction=friction, search_radius=search_radius,
14541464
)
1455-
seg_data = seg.data
1456-
if hasattr(seg_data, 'get'):
1457-
seg_vals = seg_data.get() # cupy -> numpy
1458-
else:
1459-
seg_vals = np.asarray(seg.values)
1465+
seg_vals = _segment_to_numpy(seg.data)
14601466

14611467
goal_py, goal_px = waypoint_pixels[i + 1]
14621468

@@ -1486,6 +1492,17 @@ def multi_stop_search(surface: xr.DataArray,
14861492
segment_costs.append(float(seg_goal_cost))
14871493
cumulative_cost += seg_goal_cost
14881494

1495+
# Match the input's array type, like a_star_search does
1496+
if _is_dask:
1497+
chunks = surface_data.chunks
1498+
path_data = da.from_array(path_data, chunks=chunks)
1499+
if has_cuda_and_cupy() and is_dask_cupy(surface):
1500+
import cupy
1501+
path_data = path_data.map_blocks(cupy.asarray)
1502+
elif has_cuda_and_cupy() and is_cupy_array(surface_data):
1503+
import cupy
1504+
path_data = cupy.asarray(path_data)
1505+
14891506
path_agg = xr.DataArray(
14901507
path_data,
14911508
coords=surface.coords,

xrspatial/tests/test_pathfinding.py

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

981+
# output should stay dask-backed, matching a_star_search
982+
assert isinstance(path_dask.data, da.Array)
983+
981984
np.testing.assert_allclose(
982985
np.asarray(path_dask.values),
983986
path_np.values,
@@ -999,13 +1002,49 @@ def test_multi_stop_cupy_matches_numpy():
9991002
path_np = multi_stop_search(agg_np, [wp0, wp1, wp2])
10001003
path_cupy = multi_stop_search(agg_cupy, [wp0, wp1, wp2])
10011004

1005+
# output should stay cupy-backed, matching a_star_search
1006+
import cupy
1007+
assert isinstance(path_cupy.data, cupy.ndarray)
1008+
10021009
np.testing.assert_allclose(
1003-
path_cupy.data if not hasattr(path_cupy.data, 'get') else path_cupy.data,
1010+
path_cupy.data.get(),
10041011
path_np.values,
10051012
equal_nan=True, atol=1e-10,
10061013
)
10071014

10081015

1016+
@cuda_and_cupy_available
1017+
@pytest.mark.skipif(not has_dask_array(), reason="Requires dask.Array")
1018+
def test_multi_stop_dask_cupy_matches_numpy():
1019+
"""Dask+CuPy multi-stop should not crash and should match numpy."""
1020+
data = np.ones((8, 8))
1021+
wp0 = (7.0, 0.0)
1022+
wp1 = (4.0, 3.0)
1023+
wp2 = (0.0, 7.0)
1024+
1025+
agg_np = _make_raster(data, backend='numpy')
1026+
agg_dc = _make_raster(data, backend='dask+cupy', chunks=(4, 4))
1027+
1028+
path_np = multi_stop_search(agg_np, [wp0, wp1, wp2])
1029+
path_dc = multi_stop_search(agg_dc, [wp0, wp1, wp2])
1030+
1031+
# output should stay dask-backed with cupy blocks
1032+
assert isinstance(path_dc.data, da.Array)
1033+
computed = path_dc.data.compute()
1034+
assert hasattr(computed, 'get') # cupy blocks
1035+
np.testing.assert_allclose(
1036+
computed.get(),
1037+
path_np.values,
1038+
equal_nan=True, atol=1e-10,
1039+
)
1040+
1041+
# optimize_order goes through the same extraction path
1042+
path_dc_opt = multi_stop_search(
1043+
agg_dc, [wp0, wp1, wp2], optimize_order=True)
1044+
computed_opt = path_dc_opt.data.compute()
1045+
assert np.isfinite(computed_opt.get()).any()
1046+
1047+
10091048
# =====================================================================
10101049
# Issue #1439: input validation
10111050
# =====================================================================

0 commit comments

Comments
 (0)