diff --git a/cpp/src/routing/route/capacity_route.cuh b/cpp/src/routing/route/capacity_route.cuh index 388e573c1c..3ee61c2c85 100644 --- a/cpp/src/routing/route/capacity_route.cuh +++ b/cpp/src/routing/route/capacity_route.cuh @@ -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& 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 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 { diff --git a/cpp/tests/routing/CMakeLists.txt b/cpp/tests/routing/CMakeLists.txt index 569f84beb7..57028e9fe7 100644 --- a/cpp/tests/routing/CMakeLists.txt +++ b/cpp/tests/routing/CMakeLists.txt @@ -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 diff --git a/cpp/tests/routing/unit_tests/capacity_route_resize.cu b/cpp/tests/routing/unit_tests/capacity_route_resize.cu new file mode 100644 index 0000000000..6ba5e86f61 --- /dev/null +++ b/cpp/tests/routing/unit_tests/capacity_route_resize.cu @@ -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 +#include +#include + +#include +#include + +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 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 order_locations(norders); + std::iota(order_locations.begin(), order_locations.end(), 1); // order i -> location i+1 + std::vector vehicle_start(nvehicles, 0), vehicle_return(nvehicles, 0); + std::vector demand_weight(norders, 1); + std::vector demand_volume(norders, 1); + std::vector capacity_weight(nvehicles, cap); + std::vector 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 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 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 host_route(routing_solution); + std::vector load_weight(nvehicles, 0), load_volume(nvehicles, 0); + std::vector times_served(norders, 0); + for (size_t i = 0; i < host_route.route.size(); ++i) { + auto node_type = static_cast(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; + } + } +} + +} // namespace cuopt::routing::test