Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 200 additions & 1 deletion cpp/tests/routing/unit_tests/breaks.cu
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand All @@ -9,9 +9,11 @@
#include <routing/utilities/test_utilities.hpp>

#include <cuopt/routing/solve.hpp>
#include <utilities/common_utils.hpp>
#include <utilities/copy_helpers.hpp>

#include <gtest/gtest.h>
#include <string>
#include <vector>

namespace cuopt {
Expand All @@ -36,6 +38,7 @@ TEST(vehicle_breaks, default_case)
auto v_break_locations = cuopt::device_copy(break_locations, stream);
cuopt::routing::data_model_view_t<int, float> data_model(&handle, 3, 2);
data_model.add_cost_matrix(v_cost_matrix.data());
data_model.add_transit_time_matrix(v_cost_matrix.data());
data_model.add_break_dimension(
v_break_earliest.data(), v_break_latest.data(), v_break_service.data());
data_model.set_break_locations(v_break_locations.data(), v_break_locations.size());
Expand Down Expand Up @@ -63,6 +66,7 @@ TEST(vehicle_breaks, non_default_case)

cuopt::routing::data_model_view_t<int, float> data_model(&handle, 3, 2);
data_model.add_cost_matrix(v_cost_matrix.data());
data_model.add_transit_time_matrix(v_cost_matrix.data());
data_model.add_break_dimension(
v_break_earliest.data(), v_break_latest.data(), v_break_service.data());
data_model.set_break_locations(v_break_locations.data(), v_break_locations.size());
Expand Down Expand Up @@ -232,6 +236,201 @@ TEST(vehicle_breaks, vehicle_time_windows)
}
}

// Test uniform breaks (Solomon 100 nodes)
TEST(vehicle_breaks, uniform_breaks)
{
raft::handle_t handle;
auto stream = handle.get_stream();

std::string path = cuopt::test::get_rapids_dataset_root_dir() + "/solomon/In/r107.txt";
Route<int, float> route;
load_solomon(path, route, 101);

int nodes = route.n_locations;
int n_orders = nodes - 1;
int vehicle_num = 25;

std::vector<float> cost_matrix(nodes * nodes);
build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h);

std::vector<int> order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders),
order_service(n_orders);
for (int i = 0; i < n_orders; ++i) {
order_locations[i] = i + 1;
order_earliest[i] = route.earliest_time_h[i + 1];
order_latest[i] = route.latest_time_h[i + 1];
order_service[i] = route.service_time_h[i + 1];
}
int num_breaks = 2;
std::vector<int> break_earliest(vehicle_num * num_breaks);
std::vector<int> break_latest(vehicle_num * num_breaks);
std::vector<int> break_duration(vehicle_num * num_breaks);
for (int v = 0; v < vehicle_num; ++v) {
break_earliest[v * num_breaks + 0] = 40;
break_latest[v * num_breaks + 0] = 50;
break_duration[v * num_breaks + 0] = 10;
break_earliest[v * num_breaks + 1] = 170;
break_latest[v * num_breaks + 1] = 180;
break_duration[v * num_breaks + 1] = 10;
}

std::vector<int> break_locations(nodes);
for (int i = 0; i < nodes; ++i) {
break_locations[i] = i;
}

cuopt::routing::data_model_view_t<int, float> data_model(&handle, nodes, vehicle_num, n_orders);

auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream);
auto v_order_locations = cuopt::device_copy(order_locations, stream);
auto v_order_earliest = cuopt::device_copy(order_earliest, stream);
auto v_order_latest = cuopt::device_copy(order_latest, stream);
auto v_order_service = cuopt::device_copy(order_service, stream);
auto v_break_locations = cuopt::device_copy(break_locations, stream);

data_model.add_cost_matrix(v_cost_matrix.data());
data_model.add_transit_time_matrix(v_cost_matrix.data());
data_model.set_order_locations(v_order_locations.data());
data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data());
data_model.set_order_service_times(v_order_service.data());
data_model.set_break_locations(v_break_locations.data(), v_break_locations.size());

std::vector<int> dim0_earliest(vehicle_num), dim0_latest(vehicle_num), dim0_duration(vehicle_num);
std::vector<int> dim1_earliest(vehicle_num), dim1_latest(vehicle_num), dim1_duration(vehicle_num);
for (int v = 0; v < vehicle_num; ++v) {
dim0_earliest[v] = break_earliest[v * num_breaks + 0];
dim0_latest[v] = break_latest[v * num_breaks + 0];
dim0_duration[v] = break_duration[v * num_breaks + 0];
dim1_earliest[v] = break_earliest[v * num_breaks + 1];
dim1_latest[v] = break_latest[v * num_breaks + 1];
dim1_duration[v] = break_duration[v * num_breaks + 1];
}
auto v_break_earliest_0 = cuopt::device_copy(dim0_earliest, stream);
auto v_break_latest_0 = cuopt::device_copy(dim0_latest, stream);
auto v_break_duration_0 = cuopt::device_copy(dim0_duration, stream);
data_model.add_break_dimension(
v_break_earliest_0.data(), v_break_latest_0.data(), v_break_duration_0.data());

auto v_break_earliest_1 = cuopt::device_copy(dim1_earliest, stream);
auto v_break_latest_1 = cuopt::device_copy(dim1_latest, stream);
auto v_break_duration_1 = cuopt::device_copy(dim1_duration, stream);
data_model.add_break_dimension(
v_break_earliest_1.data(), v_break_latest_1.data(), v_break_duration_1.data());

cuopt::routing::solver_settings_t<int, float> settings;
settings.set_time_limit(30);

auto routing_solution = cuopt::routing::solve(data_model, settings);
handle.sync_stream();

ASSERT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS);
host_assignment_t<int> h_routing_solution(routing_solution);
check_route(data_model, h_routing_solution);
}

// Test non-uniform breaks (Solomon 100 nodes)
TEST(vehicle_breaks, non_uniform_breaks)
{
raft::handle_t handle;
auto stream = handle.get_stream();

std::string path = cuopt::test::get_rapids_dataset_root_dir() + "/solomon/In/r107.txt";
Route<int, float> route;
load_solomon(path, route, 101);

int nodes = route.n_locations;
int n_orders = nodes - 1;
int vehicle_num = 30;

std::vector<float> cost_matrix(nodes * nodes);
build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h);

std::vector<int> order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders),
order_service(n_orders);
for (int i = 0; i < n_orders; ++i) {
order_locations[i] = i + 1;
order_earliest[i] = route.earliest_time_h[i + 1];
order_latest[i] = route.latest_time_h[i + 1];
order_service[i] = route.service_time_h[i + 1];
}
int num_v_type_1 = vehicle_num / 2;
int num_v_type_2 = vehicle_num - num_v_type_1;
int num_breaks = 3;

// Type 1: [40,50]/5, [100,120]/20, [170,180]/10
// Type 2: [60,90]/20, [110,120]/10, [200,210]/5
std::vector<int> break_earliest(vehicle_num * num_breaks);
std::vector<int> break_latest(vehicle_num * num_breaks);
std::vector<int> break_duration(vehicle_num * num_breaks);
for (int v = 0; v < num_v_type_1; ++v) {
break_earliest[v * num_breaks + 0] = 40;
break_latest[v * num_breaks + 0] = 50;
break_duration[v * num_breaks + 0] = 5;
break_earliest[v * num_breaks + 1] = 100;
break_latest[v * num_breaks + 1] = 120;
break_duration[v * num_breaks + 1] = 20;
break_earliest[v * num_breaks + 2] = 170;
break_latest[v * num_breaks + 2] = 180;
break_duration[v * num_breaks + 2] = 10;
}
for (int v = num_v_type_1; v < vehicle_num; ++v) {
break_earliest[v * num_breaks + 0] = 60;
break_latest[v * num_breaks + 0] = 90;
break_duration[v * num_breaks + 0] = 20;
break_earliest[v * num_breaks + 1] = 110;
break_latest[v * num_breaks + 1] = 120;
break_duration[v * num_breaks + 1] = 10;
break_earliest[v * num_breaks + 2] = 200;
break_latest[v * num_breaks + 2] = 210;
break_duration[v * num_breaks + 2] = 5;
}

// Depot (0) excluded from break locations
std::vector<int> break_locations(nodes - 1);
for (int i = 0; i < nodes - 1; ++i) {
break_locations[i] = i + 1;
}

cuopt::routing::data_model_view_t<int, float> data_model(&handle, nodes, vehicle_num, n_orders);

auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream);
auto v_order_locations = cuopt::device_copy(order_locations, stream);
auto v_order_earliest = cuopt::device_copy(order_earliest, stream);
auto v_order_latest = cuopt::device_copy(order_latest, stream);
auto v_order_service = cuopt::device_copy(order_service, stream);
auto v_break_locations = cuopt::device_copy(break_locations, stream);

data_model.add_cost_matrix(v_cost_matrix.data());
data_model.add_transit_time_matrix(v_cost_matrix.data());
data_model.set_order_locations(v_order_locations.data());
data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data());
data_model.set_order_service_times(v_order_service.data());
data_model.set_break_locations(v_break_locations.data(), v_break_locations.size());

for (int b = 0; b < num_breaks; ++b) {
std::vector<int> e(vehicle_num), l(vehicle_num), d(vehicle_num);
for (int v = 0; v < vehicle_num; ++v) {
e[v] = break_earliest[v * num_breaks + b];
l[v] = break_latest[v * num_breaks + b];
d[v] = break_duration[v * num_breaks + b];
}
auto v_e = cuopt::device_copy(e, stream);
auto v_l = cuopt::device_copy(l, stream);
auto v_d = cuopt::device_copy(d, stream);
data_model.add_break_dimension(v_e.data(), v_l.data(), v_d.data());
}
Comment on lines +410 to +421
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how add_break_dimension stores data (pointer vs copy)
rg -n -A10 "add_break_dimension" --type-add 'cuda:*.{cu,cuh}' --type-add 'cpp:*.{cpp,hpp,h}' -t cuda -t cpp | head -100

# Check device_copy return type
rg -n -A5 "device_copy" --type-add 'cuda:*.{cu,cuh}' --type-add 'cpp:*.{cpp,hpp,h}' -t cuda -t cpp | grep -E "(rmm|device_uvector|return)" | head -20

Repository: NVIDIA/cuopt

Length of output: 10389


🏁 Script executed:

#!/bin/bash
# Get full implementation of add_break_dimension
cat -n cpp/src/routing/data_model_view.cu | sed -n '113,160p'

# Check if there's any storage of pointers in data_model_view_t
rg -n "break_earliest|break_latest|break_duration" cpp/src/routing/data_model_view.cu | head -30

Repository: NVIDIA/cuopt

Length of output: 4201


🏁 Script executed:

#!/bin/bash
# Check how break_dimensions_ is used during solve
rg -n "break_dimensions_" cpp/src/routing/data_model_view.cu | head -20

# Check the definition of break_dimensions_ to understand the data structure
rg -n "break_dimensions_" cpp/include/cuopt/routing/data_model_view.hpp | head -20

Repository: NVIDIA/cuopt

Length of output: 316


🏁 Script executed:

#!/bin/bash
# Find break_dimension_t definition
rg -n "struct break_dimension_t|class break_dimension_t" cpp/include cpp/src -A 10 | head -40

Repository: NVIDIA/cuopt

Length of output: 1023


🏁 Script executed:

#!/bin/bash
# Check the full non_uniform_breaks test to see solve call
sed -n '329,425p' cpp/tests/routing/unit_tests/breaks.cu | cat -n

Repository: NVIDIA/cuopt

Length of output: 4815


Critical use-after-free: device memory freed before solve accesses stored pointers.

Device copies v_e, v_l, v_d are destroyed at the end of each loop iteration, but add_break_dimension stores raw pointers to this memory in break_dimensions_ (line 146 of data_model_view.cu). When solve is called at line 94, these pointers become dangling references to freed device memory.

All other tests in this file keep device memory alive until after solve completes (e.g., uniform_breaks at lines 42–47).

🐛 Proposed fix: keep device memory alive until after solve
+  std::vector<rmm::device_uvector<int>> break_device_data;
+  break_device_data.reserve(num_breaks * 3);
+
   for (int b = 0; b < num_breaks; ++b) {
     std::vector<int> e(vehicle_num), l(vehicle_num), d(vehicle_num);
     for (int v = 0; v < vehicle_num; ++v) {
       e[v] = break_earliest[v * num_breaks + b];
       l[v] = break_latest[v * num_breaks + b];
       d[v] = break_duration[v * num_breaks + b];
     }
-    auto v_e = cuopt::device_copy(e, stream);
-    auto v_l = cuopt::device_copy(l, stream);
-    auto v_d = cuopt::device_copy(d, stream);
-    data_model.add_break_dimension(v_e.data(), v_l.data(), v_d.data());
+    break_device_data.push_back(cuopt::device_copy(e, stream));
+    break_device_data.push_back(cuopt::device_copy(l, stream));
+    break_device_data.push_back(cuopt::device_copy(d, stream));
+    data_model.add_break_dimension(
+      break_device_data[b * 3].data(),
+      break_device_data[b * 3 + 1].data(),
+      break_device_data[b * 3 + 2].data());
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (int b = 0; b < num_breaks; ++b) {
std::vector<int> e(vehicle_num), l(vehicle_num), d(vehicle_num);
for (int v = 0; v < vehicle_num; ++v) {
e[v] = break_earliest[v * num_breaks + b];
l[v] = break_latest[v * num_breaks + b];
d[v] = break_duration[v * num_breaks + b];
}
auto v_e = cuopt::device_copy(e, stream);
auto v_l = cuopt::device_copy(l, stream);
auto v_d = cuopt::device_copy(d, stream);
data_model.add_break_dimension(v_e.data(), v_l.data(), v_d.data());
}
std::vector<rmm::device_uvector<int>> break_device_data;
break_device_data.reserve(num_breaks * 3);
for (int b = 0; b < num_breaks; ++b) {
std::vector<int> e(vehicle_num), l(vehicle_num), d(vehicle_num);
for (int v = 0; v < vehicle_num; ++v) {
e[v] = break_earliest[v * num_breaks + b];
l[v] = break_latest[v * num_breaks + b];
d[v] = break_duration[v * num_breaks + b];
}
break_device_data.push_back(cuopt::device_copy(e, stream));
break_device_data.push_back(cuopt::device_copy(l, stream));
break_device_data.push_back(cuopt::device_copy(d, stream));
data_model.add_break_dimension(
break_device_data[b * 3].data(),
break_device_data[b * 3 + 1].data(),
break_device_data[b * 3 + 2].data());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/routing/unit_tests/breaks.cu` around lines 406 - 417, The loop
creates temporary device copies v_e, v_l, v_d and passes their raw pointers to
data_model.add_break_dimension, but those temporaries are destroyed each
iteration leaving break_dimensions_ with dangling device pointers; fix by
hoisting storage so the device vectors outlive solve — declare a container
(e.g., std::vector<cuopt::device_vector<int>> device_storage) before the
for-loop, move or emplace each v_e, v_l, v_d into device_storage and pass the
.data() from the stored objects to add_break_dimension, ensuring device_storage
remains alive until after calling solve so break_dimensions_ pointers remain
valid.


cuopt::routing::solver_settings_t<int, float> settings;
settings.set_time_limit(30);

auto routing_solution = cuopt::routing::solve(data_model, settings);
handle.sync_stream();

ASSERT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS);
host_assignment_t<int> h_routing_solution(routing_solution);
check_route(data_model, h_routing_solution);
}

} // namespace test
} // namespace routing
} // namespace cuopt
84 changes: 83 additions & 1 deletion cpp/tests/routing/unit_tests/heterogenous_breaks.cu
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand All @@ -9,10 +9,12 @@
#include <routing/utilities/test_utilities.hpp>

#include <cuopt/routing/solve.hpp>
#include <utilities/common_utils.hpp>
#include <utilities/copy_helpers.hpp>

#include <gtest/gtest.h>
#include <random>
#include <string>
#include <vector>

namespace cuopt {
Expand Down Expand Up @@ -111,6 +113,86 @@ TEST(heterogenous_breaks, simple_non_uniform)
check_route(data_model, h_routing_solution);
}

// Test heterogenous breaks (Solomon 100 nodes):
// Half of vehicles have 2 breaks with custom locations; remaining half have 3 breaks with default
// (any) location.
TEST(heterogenous_breaks, test_heterogeneous_breaks)
{
raft::handle_t handle;
auto stream = handle.get_stream();

const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir();
std::string routing_file = rapidsDatasetRootDir + "/solomon/In/r107.txt";
Route<int, float> route;
load_solomon(routing_file, route, 101);

int nodes = route.n_locations;
int n_orders = nodes - 1;
int vehicle_num = 20;
int num_v_type_1 = vehicle_num / 2;

std::vector<float> cost_matrix(nodes * nodes), time_matrix(nodes * nodes);
build_dense_matrix(cost_matrix.data(), route.x_h, route.y_h);
build_dense_matrix(time_matrix.data(), route.x_h, route.y_h);

std::vector<int> order_locations(n_orders), order_earliest(n_orders), order_latest(n_orders),
order_service(n_orders);
for (int i = 0; i < n_orders; ++i) {
order_locations[i] = i + 1;
order_earliest[i] = route.earliest_time_h[i + 1];
order_latest[i] = route.latest_time_h[i + 1];
order_service[i] = route.service_time_h[i + 1];
}

// Type 1: 2 breaks [90,100]/15, [150,170]/15 at locations 5,10,15,...,85 (every 5th node,
// excluding depot)
std::vector<int> break_locations_1;
for (int i = 1; i <= 17; ++i) {
break_locations_1.push_back(5 * i);
}

cuopt::routing::data_model_view_t<int, float> data_model(&handle, nodes, vehicle_num, n_orders);

auto v_cost_matrix = cuopt::device_copy(cost_matrix, stream);
auto v_time_matrix = cuopt::device_copy(time_matrix, stream);
auto v_order_locations = cuopt::device_copy(order_locations, stream);
auto v_order_earliest = cuopt::device_copy(order_earliest, stream);
auto v_order_latest = cuopt::device_copy(order_latest, stream);
auto v_order_service = cuopt::device_copy(order_service, stream);

data_model.add_cost_matrix(v_cost_matrix.data());
data_model.add_transit_time_matrix(v_time_matrix.data());
data_model.set_order_locations(v_order_locations.data());
data_model.set_order_time_windows(v_order_earliest.data(), v_order_latest.data());
data_model.set_order_service_times(v_order_service.data());

auto v_break_locations_1 = cuopt::device_copy(break_locations_1, stream);

for (int i = 0; i < num_v_type_1; ++i) {
data_model.add_vehicle_break(
i, 90, 100, 15, v_break_locations_1.data(), static_cast<int>(break_locations_1.size()));
data_model.add_vehicle_break(
i, 150, 170, 15, v_break_locations_1.data(), static_cast<int>(break_locations_1.size()));
}

// Type 2: 3 breaks [40,50]/10, [110,120]/10, [160,170]/10 with default (any) location
for (int i = num_v_type_1; i < vehicle_num; ++i) {
data_model.add_vehicle_break(i, 40, 50, 10, nullptr, 0);
data_model.add_vehicle_break(i, 110, 120, 10, nullptr, 0);
data_model.add_vehicle_break(i, 160, 170, 10, nullptr, 0);
}

cuopt::routing::solver_settings_t<int, float> settings;
settings.set_time_limit(30);

auto routing_solution = cuopt::routing::solve(data_model, settings);
handle.sync_stream();

ASSERT_EQ(routing_solution.get_status(), cuopt::routing::solution_status_t::SUCCESS);
host_assignment_t<int> h_routing_solution(routing_solution);
check_route(data_model, h_routing_solution);
}

} // namespace test
} // namespace routing
} // namespace cuopt
Loading
Loading