Skip to content
Open
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
29 changes: 25 additions & 4 deletions cpp/src/routing/route/capacity_route.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,31 @@ class capacity_route_t {

void resize(i_t max_nodes_per_route, rmm::cuda_stream_view stream)
{
demand.resize(dim_info.n_capacity_dimensions * max_nodes_per_route, stream);
gathered.resize(dim_info.n_capacity_dimensions * max_nodes_per_route, stream);
max_to_node.resize(dim_info.n_capacity_dimensions * max_nodes_per_route, stream);
max_after.resize(dim_info.n_capacity_dimensions * max_nodes_per_route, stream);
i_t n_dims = dim_info.n_capacity_dimensions;
if (n_dims == 0) { return; }
auto resize_strided = [&](rmm::device_uvector<i_t>& vec) {
i_t old_stride = vec.size() / n_dims;
i_t new_stride = max_nodes_per_route;
if (old_stride == new_stride) { return; }
i_t new_cols = new_stride > 0 ? new_stride : 0;
rmm::device_uvector<i_t> new_vec(n_dims * new_cols, stream);
if (old_stride > 0 && new_stride > 0) {
// Copy the row-major data when expanding to add new columns.
RAFT_CUDA_TRY(cudaMemcpy2DAsync(new_vec.data(),
new_stride * sizeof(i_t),
vec.data(),
old_stride * sizeof(i_t),
std::min(old_stride, new_stride) * sizeof(i_t),
n_dims,
cudaMemcpyDeviceToDevice,
stream.value()));
}
vec = std::move(new_vec);
};
resize_strided(demand);
resize_strided(gathered);
resize_strided(max_to_node);
resize_strided(max_after);
}

struct view_t {
Expand Down
1 change: 1 addition & 0 deletions cpp/tests/routing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ ConfigureTest(ROUTING_UNIT_TEST
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/order_locations.cu
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/horizontal_loading.cu
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/route_constraints.cu
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/capacity_route_resize.cu
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/heterogenous_fleet.cu
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/prize_collection.cu
${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/objective_function.cu
Expand Down
100 changes: 100 additions & 0 deletions cpp/tests/routing/unit_tests/capacity_route_resize.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */

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

#include <numeric>
#include <vector>

namespace cuopt::routing::test {

// Covers capacity_route_t::resize preserving the row-major layout of a multi-dimension
// capacity route: many orders over a few vehicles force a route to grow past its base
// allocation, and a second capacity dimension exposes the resize. See issue #1618.
TEST(capacity_route_resize, multi_dim_grow_preserves_second_dimension)
{
constexpr int norders = 130;
constexpr int nvehicles = 3;
constexpr int nloops = 3;
constexpr int time_limit = 2;
constexpr int nlocations = norders + 1;
constexpr int cap = 1000000; // loose: never binding, so any excess is corruption

// Locations on a line; cost = |a - b| so consolidating orders onto a route is cheap.
std::vector<float> cost_matrix(nlocations * nlocations);
for (int a = 0; a < nlocations; ++a) {
for (int b = 0; b < nlocations; ++b) {
cost_matrix[a * nlocations + b] = std::abs(a - b);
}
}
std::vector<int> order_locations(norders);
std::iota(order_locations.begin(), order_locations.end(), 1); // order i -> location i+1
std::vector<int> vehicle_start(nvehicles, 0), vehicle_return(nvehicles, 0);
std::vector<int> demand_weight(norders, 1);
std::vector<int> demand_volume(norders, 1);
std::vector<int> capacity_weight(nvehicles, cap);
std::vector<int> capacity_volume(nvehicles, cap);

raft::handle_t handle;
auto stream = handle.get_stream();

auto v_cost_matrix = device_copy(cost_matrix, stream);
auto v_order_locations = device_copy(order_locations, stream);
auto v_start = device_copy(vehicle_start, stream);
auto v_return = device_copy(vehicle_return, stream);
auto v_demand_weight = device_copy(demand_weight, stream);
auto v_demand_volume = device_copy(demand_volume, stream);
auto v_cap_weight = device_copy(capacity_weight, stream);
auto v_cap_volume = device_copy(capacity_volume, stream);

data_model_view_t<int, float> data_model(&handle, nlocations, nvehicles, norders);
data_model.add_cost_matrix(v_cost_matrix.data());
data_model.set_order_locations(v_order_locations.data());
data_model.set_vehicle_locations(v_start.data(), v_return.data());
data_model.add_capacity_dimension("weight", v_demand_weight.data(), v_cap_weight.data());
data_model.add_capacity_dimension("volume", v_demand_volume.data(), v_cap_volume.data());

solver_settings_t<int, float> settings;
settings.set_time_limit(time_limit);

for (int loop = 0; loop < nloops; ++loop) {
auto routing_solution = solve(data_model, settings);
handle.sync_stream();
ASSERT_EQ(routing_solution.get_status(), solution_status_t::SUCCESS) << "loop " << loop;

// The in-solver feasibility assert is the primary detector (it aborts during solve on
// the corrupted dimension). These host-side checks are a secondary guard for release
// builds where asserts are compiled out: a valid solution serves every order exactly
// once and respects each configured capacity.
host_assignment_t<int> host_route(routing_solution);
std::vector<int> load_weight(nvehicles, 0), load_volume(nvehicles, 0);
std::vector<int> times_served(norders, 0);
for (size_t i = 0; i < host_route.route.size(); ++i) {
auto node_type = static_cast<node_type_t>(host_route.node_types[i]);
if (node_type == node_type_t::DEPOT || node_type == node_type_t::BREAK) { continue; }
int truck = host_route.truck_id[i];
int order = host_route.route[i];
++times_served[order];
load_weight[truck] += demand_weight[order];
load_volume[truck] += demand_volume[order];
}
for (int order = 0; order < norders; ++order) {
ASSERT_EQ(times_served[order], 1)
<< "order " << order << " served " << times_served[order] << " times, loop " << loop;
}
for (int v = 0; v < nvehicles; ++v) {
ASSERT_LE(load_weight[v], capacity_weight[v])
<< "weight cap on veh " << v << " loop " << loop;
ASSERT_LE(load_volume[v], capacity_volume[v])
<< "volume cap on veh " << v << " loop " << loop;
}
Comment thread
mlubin marked this conversation as resolved.
}
}

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