diff --git a/python/cuopt/cuopt/routing/__init__.py b/python/cuopt/cuopt/routing/__init__.py index 081d58f998..37c4c43b94 100644 --- a/python/cuopt/cuopt/routing/__init__.py +++ b/python/cuopt/cuopt/routing/__init__.py @@ -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", +] diff --git a/python/cuopt/cuopt/routing/vehicle_routing.py b/python/cuopt/cuopt/routing/vehicle_routing.py index 2a474df682..bff3aefc22 100644 --- a/python/cuopt/cuopt/routing/vehicle_routing.py +++ b/python/cuopt/cuopt/routing/vehicle_routing.py @@ -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 @@ -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, diff --git a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py index 65cddede95..077f661b93 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py @@ -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): @@ -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): diff --git a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py index 7d9b611124..a303e1d602 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py @@ -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) diff --git a/python/cuopt/cuopt/tests/routing/test_distance_engine.py b/python/cuopt/cuopt/tests/routing/test_distance_engine.py index d2cd4c199f..c027383be9 100644 --- a/python/cuopt/cuopt/tests/routing/test_distance_engine.py +++ b/python/cuopt/cuopt/tests/routing/test_distance_engine.py @@ -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 @@ -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, @@ -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( diff --git a/python/cuopt/cuopt/tests/routing/test_gpu_free_import.py b/python/cuopt/cuopt/tests/routing/test_gpu_free_import.py new file mode 100644 index 0000000000..ba7972e89b --- /dev/null +++ b/python/cuopt/cuopt/tests/routing/test_gpu_free_import.py @@ -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" + ) diff --git a/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py b/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py index d8c8f8f74c..5cc405f677 100644 --- a/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py +++ b/python/cuopt/cuopt/tests/routing/test_warnings_exceptions.py @@ -75,6 +75,21 @@ def test_dist_mat(): ) +def test_dist_mat_null(): + cost_matrix = cudf.DataFrame( + [ + [0, 5.0, 5.0, 5.0], + [5.0, 0, 5.0, 5.0], + [5.0, 5.0, 0, 5.0], + [5.0, None, 5.0, 0], + ] + ) + with pytest.raises(ValueError) as exc_info: + dm = routing.DataModel(cost_matrix.shape[0], 3) + dm.add_cost_matrix(cost_matrix) + assert str(exc_info.value) == "cost matrix cannot have NULL values" + + def test_time_windows(): cost_matrix = cudf.DataFrame( [