Skip to content
Draft
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
3 changes: 2 additions & 1 deletion cpp/src/routing/adapters/adapted_generator.cu
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ void adapted_generator_t<i_t, f_t, REQUEST>::generate_solution(
sol.sol.eject_until_feasible();
resource.ges.init_ejection_pool();
resource.ges.fixed_route_loop();
if (dim_info.has_dimension(dim_t::BREAK)) { resource.ges.try_squeeze_breaks_feasible(); }
}

resource.ges.repair_empty_routes();
// Construction can eject breaks before reinserting requests into the routes.
if (dim_info.has_dimension(dim_t::BREAK)) { resource.ges.try_squeeze_breaks_feasible(); }

sol.populate_host_data(true);
cuopt_func_call(sol.check_device_host_coherence());
Expand Down
10 changes: 10 additions & 0 deletions cpp/src/routing/adapters/adapted_modifier.cu
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ void adapted_modifier_t<i_t, f_t, REQUEST>::perturbate(
for (i_t i = 0; i < perturbation_count; ++i) {
resource.ls.run_random_local_search(adapted_solution.sol, false);
}
if (adapted_solution.problem->has_vehicle_breaks()) {
resource.ges.set_solution_ptr(&adapted_solution.sol);
resource.ges.squeeze_breaks();
}
adapted_solution.populate_host_data(true);
adapted_solution.check_device_host_coherence();
cuopt_func_call(adapted_solution.sol.check_cost_coherence(gpu_weight));
Expand All @@ -59,6 +63,12 @@ void adapted_modifier_t<i_t, f_t, REQUEST>::improve(
resource.ls.start_timer(time_limit);
resource.ls.run_best_local_search(
adapted_solution.sol, consider_unserviced, time_limit_enabled, run_cycle_finder);
// Moving requests can make a previously skipped break required, or shorten a route enough
// to omit a break. Reconcile both cases before publishing its cost and feasibility.
if (adapted_solution.problem->has_vehicle_breaks()) {
resource.ges.set_solution_ptr(&adapted_solution.sol);
resource.ges.squeeze_breaks();
}
adapted_solution.populate_host_data();
adapted_solution.check_device_host_coherence();
cuopt_func_call(adapted_solution.sol.check_cost_coherence(gpu_weight));
Expand Down
2 changes: 2 additions & 0 deletions cpp/src/routing/diversity/diverse_solver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,8 @@ struct solve {
}
next_injection = injection_it;
}
// Loading an initial solution strips its break nodes.
if (p->has_vehicle_breaks()) { lm.squeeze_breaks(temp_pair.first, final_weights); }
} else {
auto time_limit = timer.clamp_remaining_time(sol_gen_time);
g.generate_solution(
Expand Down
40 changes: 27 additions & 13 deletions cpp/src/routing/ges/squeeze.cu
Original file line number Diff line number Diff line change
Expand Up @@ -365,15 +365,18 @@ bool guided_ejection_search_t<i_t, f_t, REQUEST>::try_squeeze_feasible(
}

template <typename i_t, typename f_t, request_t REQUEST>
void guided_ejection_search_t<i_t, f_t, REQUEST>::squeeze_breaks()
void normalize_solution_breaks(solution_t<i_t, f_t, REQUEST>& solution,
infeasible_cost_t weights,
bool preserve_empty_breaks)
{
raft::common::nvtx::range fun_scope("squeeze_breaks");
solution_ptr->global_runtime_checks(false, false, "squeeze_breaks_begin");
auto stream = solution_ptr->sol_handle->get_stream();
size_t n_break_dims = solution_ptr->problem_ptr->get_max_break_dimensions();
size_t n_blocks = solution_ptr->n_routes;
size_t sh_size = solution_ptr->check_routes_can_insert_and_get_sh_size(n_break_dims) +
sizeof(i_t) * n_break_dims;
solution.global_runtime_checks(false, false, "squeeze_breaks_begin");
auto stream = solution.sol_handle->get_stream();
size_t n_break_dims = solution.problem_ptr->get_max_break_dimensions();
size_t n_blocks = solution.n_routes;
if (n_break_dims == 0 || n_blocks == 0) { return; }
size_t sh_size =
solution.check_routes_can_insert_and_get_sh_size(n_break_dims) + sizeof(i_t) * n_break_dims;
size_t TPB = 128;

if (!set_shmem_of_kernel(squeeze_breaks_kernel<i_t, f_t, REQUEST>, sh_size)) {
Expand All @@ -382,11 +385,16 @@ void guided_ejection_search_t<i_t, f_t, REQUEST>::squeeze_breaks()
}

squeeze_breaks_kernel<i_t, f_t, REQUEST><<<n_blocks, TPB, sh_size, stream.get()>>>(
solution_ptr->view(), false, local_search_ptr_->move_candidates.weights);
RAFT_CHECK_CUDA(solution_ptr->sol_handle->get_stream().get());
solution_ptr->compute_cost();
solution_ptr->global_runtime_checks(false, false, "squeeze_breaks_end");
return;
solution.view(), false, weights, preserve_empty_breaks);
RAFT_CHECK_CUDA(stream.get());
solution.compute_cost();
solution.global_runtime_checks(false, false, "squeeze_breaks_end");
}

template <typename i_t, typename f_t, request_t REQUEST>
void guided_ejection_search_t<i_t, f_t, REQUEST>::squeeze_breaks()
{
normalize_solution_breaks(*solution_ptr, local_search_ptr_->move_candidates.weights, false);
}

template <typename i_t, typename f_t, request_t REQUEST>
Expand All @@ -398,7 +406,9 @@ bool guided_ejection_search_t<i_t, f_t, REQUEST>::try_squeeze_breaks_feasible()
if (n_break_dims == 0) { return solution_ptr->is_feasible(); }
auto stream = solution_ptr->sol_handle->get_stream();

squeeze_breaks();
// Empty routes still participate in construction. Their breaks constrain future greedy
// insertions, preventing requests from repeatedly returning to an incompatible vehicle.
normalize_solution_breaks(*solution_ptr, local_search_ptr_->move_candidates.weights, true);

if (solution_ptr->is_feasible()) { return true; }

Expand All @@ -414,6 +424,8 @@ bool guided_ejection_search_t<i_t, f_t, REQUEST>::try_squeeze_breaks_feasible()

local_search_ptr_->set_active_weights(local_search_ptr_->move_candidates.weights,
original_incl_objective);
// Repair can move requests across routes and change which break deadlines are reached.
normalize_solution_breaks(*solution_ptr, local_search_ptr_->move_candidates.weights, true);
return solution_ptr->is_feasible();
}

Expand All @@ -435,6 +447,8 @@ template int guided_ejection_search_t<int, float, request_t::PDP>::try_multiple_
template int guided_ejection_search_t<int, float, request_t::VRP>::try_multiple_feasible_insertions(
int, bool);

template void guided_ejection_search_t<int, float, request_t::PDP>::squeeze_breaks();
template void guided_ejection_search_t<int, float, request_t::VRP>::squeeze_breaks();
template bool guided_ejection_search_t<int, float, request_t::PDP>::try_squeeze_breaks_feasible();
template bool guided_ejection_search_t<int, float, request_t::VRP>::try_squeeze_breaks_feasible();
} // namespace detail
Expand Down
198 changes: 139 additions & 59 deletions cpp/src/routing/ges/squeeze.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -402,21 +402,64 @@ __global__ void eject_inserted_requests(
}
}

struct break_route_end_t {
double time;
double distance;
};

// Replay the earliest schedule without time warping: adding accumulated excess to the stored
// departure time is not exact when a later wait absorbs an earlier time-window violation.
template <typename i_t, typename f_t, request_t REQUEST>
DI break_route_end_t get_break_route_end(const typename route_t<i_t, f_t, REQUEST>::view_t& route,
bool include_breaks)
{
const auto vehicle = route.vehicle_info();
const bool has_time = route.dimensions_info().has_dimension(dim_t::TIME);
auto prev = route.get_node(0);
break_route_end_t end{has_time ? prev.time_dim.window_start : 0., 0.};
for (i_t idx = 1; idx <= route.get_num_nodes(); ++idx) {
auto next = route.get_node(idx);
if (!include_breaks && next.node_info().is_break()) { continue; }
end.distance += get_distance(prev.node_info(), next.node_info(), vehicle);
if (has_time) {
end.time = max(end.time + get_transit_time(prev.node_info(), next.node_info(), vehicle, true),
next.time_dim.window_start);
}
prev = next;
}
return end;
}

template <typename i_t, typename f_t>
DI bool is_break_required(const typename special_nodes_t<i_t>::view_t& break_nodes,
const break_route_end_t& end,
bool has_time)
{
// A valid distance deadline can equal the unbounded sentinel used by time breaks.
if (!break_nodes.is_distance_break.empty() && break_nodes.is_distance_break[0]) {
return end.distance > break_nodes.distance_max[0];
}
// Without a modeled schedule, there is no completion time that justifies omitting a break.
return !has_time || end.time > break_nodes.latest_time[0];
}

/**
* @brief Inserts each missing break dimension into routes that lack it, one break per outer
* iteration, picking the least-cost insertion position for each. One block per route.
* @brief Remove unnecessary breaks and insert missing breaks whose deadlines the route exceeds.
* Revisit skipped dimensions after an insertion extends the route. One block per route.
*/
template <typename i_t, typename f_t, request_t REQUEST>
__global__ void squeeze_breaks_kernel(typename solution_t<i_t, f_t, REQUEST>::view_t solution,
const bool include_objective,
infeasible_cost_t weights)
infeasible_cost_t weights,
const bool preserve_empty_breaks)
{
extern __shared__ i_t shmem[];

i_t route_id = blockIdx.x;

auto global_route = solution.routes[route_id];
const int n_break_dims = solution.problem.get_break_dimensions(global_route.get_vehicle_id());
if (n_break_dims == 0) { return; }

typename route_t<i_t, f_t, REQUEST>::view_t sh_route;
sh_route = route_t<i_t, f_t, REQUEST>::view_t::create_shared_route(
Expand All @@ -431,69 +474,106 @@ __global__ void squeeze_breaks_kernel(typename solution_t<i_t, f_t, REQUEST>::vi
}
__syncthreads();

for (i_t tid = threadIdx.x; tid < sh_route.get_num_nodes(); tid += blockDim.x) {
auto node = sh_route.get_node(tid);
if (node.node_info().is_break()) {
cuopt_assert(node.node_info().break_dim() >= 0 && node.node_info().break_dim() < n_break_dims,
"Break dimension out of bounds");
break_dim_counters[node.node_info().break_dim()] = 1;
const bool has_time = sh_route.dimensions_info().has_dimension(dim_t::TIME);
const bool has_requests = sh_route.get_num_service_nodes() > 0;
const bool keep_empty_breaks = preserve_empty_breaks && !has_requests;
__shared__ break_route_end_t route_end;
__syncthreads();
if (threadIdx.x == 0) {
// Test existing breaks against the route without any breaks. Otherwise two optional
// detours can make each other appear required even though the work finishes before both.
const auto end_without_breaks = get_break_route_end<i_t, f_t, REQUEST>(sh_route, false);
for (i_t idx = sh_route.get_num_nodes() - 1; idx > 0; --idx) {
auto node = sh_route.get_node(idx);
if (!node.node_info().is_break()) { continue; }
const auto break_dim = node.node_info().break_dim();
cuopt_assert(break_dim >= 0 && break_dim < n_break_dims, "Break dimension out of bounds");
auto break_nodes =
solution.problem.special_nodes.subset(sh_route.get_vehicle_id(), break_dim);
if (keep_empty_breaks || (has_requests && is_break_required<i_t, f_t>(
break_nodes, end_without_breaks, has_time))) {
// Preserve placements already improved by local search.
break_dim_counters[break_dim] = 1;
} else {
sh_route.eject_node(idx, solution.route_node_map, false);
}
}
route_t<i_t, f_t, REQUEST>::view_t::compute_forward(sh_route);
route_t<i_t, f_t, REQUEST>::view_t::compute_backward(sh_route);
sh_route.compute_cost();
route_end = get_break_route_end<i_t, f_t, REQUEST>(sh_route, true);
}
__syncthreads();

for (int break_dim_idx = 0; break_dim_idx < n_break_dims; ++break_dim_idx) {
if (break_dim_counters[break_dim_idx] == 1) { continue; }
const auto old_objective_cost = sh_route.get_objective_cost();
const auto old_infeasbility_cost = sh_route.get_infeasibility_cost();
auto break_nodes =
solution.problem.special_nodes.subset(sh_route.get_vehicle_id(), break_dim_idx);

i_t route_size = sh_route.get_num_nodes();
i_t num_break_nodes = break_nodes.size();
double thread_best_cost = std::numeric_limits<double>::max();
i_t thread_best_idx = -1;
i_t thread_best_break_node_id = -1;
for (int index = threadIdx.x; index < route_size * num_break_nodes; index += blockDim.x) {
i_t break_node_id = index / route_size;
i_t insertion_idx = index % route_size;

auto break_node = create_break_node<i_t, f_t, REQUEST>(
break_nodes, break_node_id, solution.problem.dimensions_info);

auto curr_node = sh_route.get_node(insertion_idx);
auto next_node = sh_route.get_node(insertion_idx + 1);

curr_node.calculate_forward_all(break_node, sh_route.vehicle_info());

double cost_difference = break_node.calculate_forward_all_and_delta(next_node,
sh_route.vehicle_info(),
include_objective,
weights,
old_objective_cost,
old_infeasbility_cost);
if (cost_difference < thread_best_cost) {
thread_best_cost = cost_difference;
thread_best_idx = insertion_idx;
thread_best_break_node_id = break_node_id;
// Each successful pass inserts at least one previously missing dimension, so at most
// n_break_dims passes are needed, including when dimensions are not in deadline order.
__shared__ bool inserted_break;
for (int pass = 0; (has_requests || keep_empty_breaks) && pass < n_break_dims; ++pass) {
if (threadIdx.x == 0) { inserted_break = false; }
__syncthreads();
for (int break_dim_idx = 0; break_dim_idx < n_break_dims; ++break_dim_idx) {
__syncthreads();
if (break_dim_counters[break_dim_idx] == 1) { continue; }
const auto old_objective_cost = sh_route.get_objective_cost();
const auto old_infeasbility_cost = sh_route.get_infeasibility_cost();
auto break_nodes =
solution.problem.special_nodes.subset(sh_route.get_vehicle_id(), break_dim_idx);
if (!keep_empty_breaks && !is_break_required<i_t, f_t>(break_nodes, route_end, has_time)) {
continue;
}
}

i_t t_id = threadIdx.x;
__shared__ i_t reduction_idx;
__shared__ double reduction_buf[2 * raft::WarpSize];
block_reduce_ranked(thread_best_cost, t_id, reduction_buf, &reduction_idx);

if (threadIdx.x == reduction_idx && thread_best_break_node_id >= 0 &&
reduction_buf[0] != std::numeric_limits<double>::max()) {
auto break_node = create_break_node<i_t, f_t, REQUEST>(
break_nodes, thread_best_break_node_id, solution.problem.dimensions_info);
// do not update the intra indices yet
sh_route.insert_node(thread_best_idx, break_node, solution.route_node_map, false);
route_t<i_t, f_t, REQUEST>::view_t::compute_forward(sh_route);
route_t<i_t, f_t, REQUEST>::view_t::compute_backward(sh_route);
sh_route.compute_cost();
}
i_t route_size = sh_route.get_num_nodes();
i_t num_break_nodes = break_nodes.size();
double thread_best_cost = std::numeric_limits<double>::max();
i_t thread_best_idx = -1;
i_t thread_best_break_node_id = -1;
for (int index = threadIdx.x; index < route_size * num_break_nodes; index += blockDim.x) {
i_t break_node_id = index / route_size;
i_t insertion_idx = index % route_size;

auto break_node = create_break_node<i_t, f_t, REQUEST>(
break_nodes, break_node_id, solution.problem.dimensions_info);

auto curr_node = sh_route.get_node(insertion_idx);
auto next_node = sh_route.get_node(insertion_idx + 1);

curr_node.calculate_forward_all(break_node, sh_route.vehicle_info());

double cost_difference = break_node.calculate_forward_all_and_delta(next_node,
sh_route.vehicle_info(),
include_objective,
weights,
old_objective_cost,
old_infeasbility_cost);
if (cost_difference < thread_best_cost) {
thread_best_cost = cost_difference;
thread_best_idx = insertion_idx;
thread_best_break_node_id = break_node_id;
}
}

i_t t_id = threadIdx.x;
__shared__ i_t reduction_idx;
__shared__ double reduction_buf[2 * raft::WarpSize];
block_reduce_ranked(thread_best_cost, t_id, reduction_buf, &reduction_idx);

if (threadIdx.x == reduction_idx && thread_best_break_node_id >= 0 &&
reduction_buf[0] != std::numeric_limits<double>::max()) {
auto break_node = create_break_node<i_t, f_t, REQUEST>(
break_nodes, thread_best_break_node_id, solution.problem.dimensions_info);
// do not update the intra indices yet
sh_route.insert_node(thread_best_idx, break_node, solution.route_node_map, false);
route_t<i_t, f_t, REQUEST>::view_t::compute_forward(sh_route);
route_t<i_t, f_t, REQUEST>::view_t::compute_backward(sh_route);
sh_route.compute_cost();
route_end = get_break_route_end<i_t, f_t, REQUEST>(sh_route, true);
break_dim_counters[break_dim_idx] = 1;
inserted_break = true;
}

__syncthreads();
}
if (!inserted_break) { break; }
__syncthreads();
}

Expand Down
4 changes: 3 additions & 1 deletion cpp/src/routing/local_search/breaks_insertion.cu
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ __global__ void find_break_insertions_kernel(
i_t route_id = blockIdx.x / n_max_break_dims;
i_t ejected_break_dim = blockIdx.x % n_max_break_dims;
auto global_route = solution.routes[route_id];
if (ejected_break_dim >= global_route.get_num_breaks()) { return; }
if (ejected_break_dim >= solution.problem.get_break_dimensions(global_route.get_vehicle_id())) {
return;
}

for (int i = 0; i < global_route.get_num_nodes(); ++i) {
auto node = global_route.get_node(i);
Expand Down
5 changes: 5 additions & 0 deletions cpp/src/routing/problem/problem.cu
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ void problem_t<i_t, f_t>::populate_special_nodes()
std::vector<NodeInfo<>> node_infos_h;
std::vector<i_t> node_earliest_h, node_latest_h;
std::vector<float> node_distance_min_h, node_distance_max_h;
std::vector<uint8_t> node_is_distance_break_h;
std::vector<i_t> break_loc_to_idx_h;

if (!uniform_breaks.empty()) {
Expand Down Expand Up @@ -647,6 +648,7 @@ void problem_t<i_t, f_t>::populate_special_nodes()
node_latest_h.reserve(2 * n_vehicles);
node_distance_min_h.reserve(2 * n_vehicles);
node_distance_max_h.reserve(2 * n_vehicles);
node_is_distance_break_h.reserve(2 * n_vehicles);

break_nodes_offset_h.push_back(0);

Expand Down Expand Up @@ -708,6 +710,7 @@ void problem_t<i_t, f_t>::populate_special_nodes()
node_latest_h.push_back(break_latest_h[v][dim]);
node_distance_min_h.push_back(vehicle_break.distance_min_);
node_distance_max_h.push_back(vehicle_break.distance_max_);
node_is_distance_break_h.push_back(vehicle_break.is_distance_based_);
}

break_nodes_offset_h.push_back(offset);
Expand Down Expand Up @@ -753,6 +756,8 @@ void problem_t<i_t, f_t>::populate_special_nodes()
if (special_nodes.has_distance_break) {
special_nodes.distance_min = cuopt::device_copy(node_distance_min_h, handle_ptr->get_stream());
special_nodes.distance_max = cuopt::device_copy(node_distance_max_h, handle_ptr->get_stream());
special_nodes.is_distance_break =
cuopt::device_copy(node_is_distance_break_h, handle_ptr->get_stream());
}
special_nodes.break_loc_to_idx = cuopt::device_copy(break_loc_to_idx_h, handle_ptr->get_stream());
RAFT_CHECK_CUDA(handle_ptr->get_stream().get());
Expand Down
Loading