Skip to content

Commit ee1f26b

Browse files
Make cuopt.routing importable without a GPU
Importing cuopt.routing pulled in cuopt.routing.utils / utils_wrapper, whose generate_dataset had cudf.Series() as default argument values. Default arguments are evaluated at import time, and constructing an (even empty) cudf.Series requires a GPU, so `import cuopt.routing` failed with cudaErrorNoDevice on a GPU-less host -- e.g. a CPU-only client building and serializing a routing problem to submit to a remote solver. Move the empty-series construction from the signatures into the function bodies via None sentinels. `import cudf` itself does not touch the GPU, so the modules now import GPU-free; the helpers behave identically when called. Add __all__ to the routing package for explicit re-exports, and a subprocess test that imports the package with no visible GPU. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
1 parent 37a7b63 commit ee1f26b

4 files changed

Lines changed: 78 additions & 10 deletions

File tree

python/cuopt/cuopt/routing/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,26 @@
99
update_routes_and_vehicles,
1010
)
1111
from cuopt.routing.utils_wrapper import DatasetDistribution
12-
from cuopt.routing.vehicle_routing import BatchSolve, DataModel, Solve, SolverSettings
12+
from cuopt.routing.vehicle_routing import (
13+
BatchSolve,
14+
DataModel,
15+
Solve,
16+
SolverSettings,
17+
)
1318
from cuopt.routing.vehicle_routing_wrapper import ErrorStatus, Objective
19+
20+
__all__ = [
21+
"Assignment",
22+
"BatchSolve",
23+
"DataModel",
24+
"DatasetDistribution",
25+
"ErrorStatus",
26+
"Objective",
27+
"SolutionStatus",
28+
"Solve",
29+
"SolverSettings",
30+
"add_vehicle_constraints",
31+
"create_pickup_delivery_data",
32+
"generate_dataset",
33+
"update_routes_and_vehicles",
34+
]

python/cuopt/cuopt/routing/utils.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

44
import glob
@@ -17,10 +17,10 @@
1717
def generate_dataset(
1818
locations=100,
1919
asymmetric=True,
20-
min_demand=cudf.Series(),
21-
max_demand=cudf.Series(),
22-
min_capacities=cudf.Series(),
23-
max_capacities=cudf.Series(),
20+
min_demand=None,
21+
max_demand=None,
22+
min_capacities=None,
23+
max_capacities=None,
2424
min_service_time=0,
2525
max_service_time=0,
2626
tw_tightness=0.0,
@@ -114,6 +114,18 @@ def generate_dataset(
114114
Time windows and multi dimension
115115
capacity for each vehicle.
116116
"""
117+
# Default to empty device series here rather than in the signature:
118+
# a cudf.Series() default is constructed at import time and needs a GPU,
119+
# which would make importing this module fail on a GPU-less host.
120+
if min_demand is None:
121+
min_demand = cudf.Series()
122+
if max_demand is None:
123+
max_demand = cudf.Series()
124+
if min_capacities is None:
125+
min_capacities = cudf.Series()
126+
if max_capacities is None:
127+
max_capacities = cudf.Series()
128+
117129
if (
118130
min_demand.shape[0] != max_demand.shape[0]
119131
or max_demand.shape[0] != min_capacities.shape[0]

python/cuopt/cuopt/routing/utils_wrapper.pyx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa
1+
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

44

@@ -35,15 +35,27 @@ class DatasetDistribution(IntEnum):
3535
RANDOM_CLUSTERED = dataset_distribution_t.RANDOM_CLUSTERED
3636

3737

38-
def generate_dataset(locations=100, asymmetric=True, min_demand=cudf.Series(),
39-
max_demand=cudf.Series(), min_capacities=cudf.Series(),
40-
max_capacities=cudf.Series(), min_service_time=0,
38+
def generate_dataset(locations=100, asymmetric=True, min_demand=None,
39+
max_demand=None, min_capacities=None,
40+
max_capacities=None, min_service_time=0,
4141
max_service_time=0, tw_tightness=0.0,
4242
drop_return_trips=0.0, shifts=1,
4343
n_vehicle_types=1, n_matrix_types=1,
4444
distribution=DatasetDistribution.CLUSTERED,
4545
center_box=None, seed=0):
4646

47+
# Default to empty device series here rather than in the signature:
48+
# a cudf.Series() default is constructed at import time and needs a GPU,
49+
# which would make importing this module fail on a GPU-less host.
50+
if min_demand is None:
51+
min_demand = cudf.Series()
52+
if max_demand is None:
53+
max_demand = cudf.Series()
54+
if min_capacities is None:
55+
min_capacities = cudf.Series()
56+
if max_capacities is None:
57+
max_capacities = cudf.Series()
58+
4759
cdef unique_ptr[handle_t] handle_ptr
4860
handle_ptr.reset(new handle_t())
4961
handle_ = handle_ptr.get()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import os
5+
import subprocess
6+
import sys
7+
8+
9+
def test_routing_imports_without_a_gpu():
10+
"""cuopt.routing (and its dataset helpers) must import on a GPU-less host.
11+
12+
The dataset helpers build empty cudf.Series objects; doing so in a default
13+
argument would run at import time and fail with cudaErrorNoDevice where no
14+
GPU is visible. This guards that the construction stays inside the function
15+
bodies. Run in a subprocess with no visible GPU for a faithful check.
16+
"""
17+
code = (
18+
"import cuopt.routing as r\n"
19+
"assert callable(r.generate_dataset)\n"
20+
"assert r.DatasetDistribution.CLUSTERED is not None\n"
21+
)
22+
env = {**os.environ, "CUDA_VISIBLE_DEVICES": ""}
23+
subprocess.run([sys.executable, "-c", code], check=True, env=env)

0 commit comments

Comments
 (0)