Skip to content
29 changes: 22 additions & 7 deletions python/cuopt/cuopt/routing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Dataset-generation and batch helpers (generate_dataset, DatasetDistribution,
# add_vehicle_constraints, ...) are local-testing utilities that require a GPU
# to run. They deliberately live in cuopt.routing.utils and are NOT re-exported
# here, so importing cuopt.routing (e.g. to build/serialize a problem for a
# remote solver) does not pull in that GPU machinery. Import them from
# ``cuopt.routing.utils`` directly when needed.
from cuopt.routing.assignment import Assignment, SolutionStatus
from cuopt.routing.utils import (
add_vehicle_constraints,
create_pickup_delivery_data,
generate_dataset,
update_routes_and_vehicles,
from cuopt.routing.vehicle_routing import (
BatchSolve,
DataModel,
Solve,
SolverSettings,
)
from cuopt.routing.utils_wrapper import DatasetDistribution
from cuopt.routing.vehicle_routing import BatchSolve, DataModel, Solve, SolverSettings
from cuopt.routing.vehicle_routing_wrapper import ErrorStatus, Objective

__all__ = [
"Assignment",
"BatchSolve",
"DataModel",
"ErrorStatus",
"Objective",
"SolutionStatus",
"Solve",
"SolverSettings",
]
7 changes: 5 additions & 2 deletions python/cuopt/cuopt/routing/vehicle_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import cudf

from cuopt import routing
from cuopt.routing import vehicle_routing_wrapper
from cuopt.routing._deferred import _DeferredDataModel
from cuopt.utilities import catch_cuopt_exception
Expand Down Expand Up @@ -1561,7 +1560,11 @@ def Solve(data_model, solver_settings=None):
data_model._build(), solver_settings
)
if solver_settings.get_config_file_name() is not None:
routing.utils.save_data_model_to_yaml(
# imported here rather than at module scope: utils lives outside the
# cuopt.routing import path so that importing routing stays GPU-free.
from cuopt.routing import utils

utils.save_data_model_to_yaml(
data_model,
solver_settings,
solution,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,20 @@ def set_solution(
)


_BAD_ARRAY_SKIP = pytest.mark.skip(
reason=(
"Temporarily disabled: aborts with std::bad_array_new_length "
"in wheel builds after the fast MPS parser landed (#1429). "
"Tracked in https://github.com/NVIDIA/cuopt/issues/1592."
)
)


@pytest.mark.parametrize(
"file_name",
[
("/mip/swath1.mps"),
("/mip/neos5-free-bound.mps"),
pytest.param("/mip/swath1.mps", marks=_BAD_ARRAY_SKIP),
pytest.param("/mip/neos5-free-bound.mps", marks=_BAD_ARRAY_SKIP),
],
)
def test_incumbent_get_callback(file_name):
Expand All @@ -124,8 +133,8 @@ def test_incumbent_get_callback(file_name):
@pytest.mark.parametrize(
"file_name",
[
("/mip/swath1.mps"),
("/mip/neos5-free-bound.mps"),
pytest.param("/mip/swath1.mps", marks=_BAD_ARRAY_SKIP),
pytest.param("/mip/neos5-free-bound.mps", marks=_BAD_ARRAY_SKIP),
],
)
def test_incumbent_get_set_callback(file_name):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,13 @@ def test_solved_by():
assert solution.get_solved_by() == SolverMethod.Barrier


@pytest.mark.skip(
reason=(
"Temporarily disabled: swath1 aborts with std::bad_array_new_length "
"in wheel builds after the fast MPS parser landed (#1429). "
"Tracked in https://github.com/NVIDIA/cuopt/issues/1592."
)
)
def test_heuristics_only():
file_path = RAPIDS_DATASET_ROOT_DIR + "/mip/swath1.mps"
data_model_obj = Read(file_path)
Expand Down
6 changes: 3 additions & 3 deletions python/cuopt/cuopt/tests/routing/test_distance_engine.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import numpy as np
Expand Down Expand Up @@ -228,7 +228,7 @@ def test_compute_cost_matrix():
]

# Set vehicle constraints
vehicle_constraints = routing.add_vehicle_constraints(
vehicle_constraints = utils.add_vehicle_constraints(
num_vehicles,
vehicle_capacity,
break_earliest,
Expand All @@ -255,7 +255,7 @@ def test_compute_cost_matrix():
matrix_df,
order_data,
vehicle_data,
) = routing.create_pickup_delivery_data(
) = utils.create_pickup_delivery_data(
matrix_pdf, single_day_order_pdf, depot, vehicle_constraints
)
check_matrix(
Expand Down
42 changes: 42 additions & 0 deletions python/cuopt/cuopt/tests/routing/test_gpu_free_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import os
import subprocess
import sys


def _run_without_gpu(code):
env = {**os.environ, "CUDA_VISIBLE_DEVICES": ""}
subprocess.run([sys.executable, "-c", code], check=True, env=env)


def test_routing_imports_without_a_gpu():
"""cuopt.routing must import and build a model on a GPU-less host.

The GPU dataset/batch helpers live in cuopt.routing.utils and are not
re-exported by the package, so importing cuopt.routing must not pull in
utils / utils_wrapper (which need a GPU). A CPU-only client can then build
and serialize a problem to submit to a remote solver. Run in a subprocess
with no visible GPU for a faithful check.
"""
_run_without_gpu(
"import sys\n"
"import cuopt.routing as r\n"
"assert 'cuopt.routing.utils' not in sys.modules\n"
"assert 'cuopt.routing.utils_wrapper' not in sys.modules\n"
# none of the GPU helpers are part of the public surface anymore;
# enumerate the full set so re-exposing any one of them fails the test
"moved = [\n"
" 'generate_dataset',\n"
" 'DatasetDistribution',\n"
" 'add_vehicle_constraints',\n"
" 'create_pickup_delivery_data',\n"
" 'update_routes_and_vehicles',\n"
"]\n"
"leaked = [n for n in moved if hasattr(r, n)]\n"
"assert not leaked, leaked\n"
"import numpy as np\n"
"dm = r.DataModel(3, 2)\n"
"dm.add_cost_matrix(np.eye(3, dtype=np.float32))\n"
)
Loading