diff --git a/cpp/include/cuopt/linear_programming/cuopt_c.h b/cpp/include/cuopt/linear_programming/cuopt_c.h index 06af2ae86..028cba822 100644 --- a/cpp/include/cuopt/linear_programming/cuopt_c.h +++ b/cpp/include/cuopt/linear_programming/cuopt_c.h @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -681,6 +681,56 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings, const char* parameter_name, cuopt_float_t* parameter_value); +/** + * @brief Callback for receiving incumbent MIP solutions with user context. + * + * @param[in] solution - Device pointer to incumbent solution values. + * @param[in] objective_value - Device pointer to incumbent objective value. + * @param[in] user_data - User context pointer. + */ +typedef void (*cuOptMipGetSolutionCallback)(const cuopt_float_t* solution, + const cuopt_float_t* objective_value, + void* user_data); + +/** + * @brief Callback for injecting MIP solutions with user context. + * + * @param[out] solution - Device pointer to solution values to set. + * @param[out] objective_value - Device pointer to objective value to set. + * @param[in] user_data - User context pointer. + */ +typedef void (*cuOptMipSetSolutionCallback)(cuopt_float_t* solution, + cuopt_float_t* objective_value, + void* user_data); + +/** + * @brief Register a callback to receive incumbent MIP solutions. + * + * @param[in] settings - The solver settings object. + * @param[in] callback - Callback function to receive incumbent solutions. + * @param[in] user_data - User-defined pointer passed through to the callback. + * It will be forwarded to ``cuOptMipGetSolutionCallback`` when invoked. + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptSetMipGetSolutionCallback(cuOptSolverSettings settings, + cuOptMipGetSolutionCallback callback, + void* user_data); + +/** + * @brief Register a callback to inject MIP solutions. + * + * @param[in] settings - The solver settings object. + * @param[in] callback - Callback function to inject solutions. + * @param[in] user_data - User-defined pointer passed through to the callback. + * It will be forwarded to ``cuOptMipSetSolutionCallback`` when invoked. + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptSetMipSetSolutionCallback(cuOptSolverSettings settings, + cuOptMipSetSolutionCallback callback, + void* user_data); + /** @brief Check if an optimization problem is a mixed integer programming problem. * * @param[in] problem - The optimization problem. diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 4f6320752..11a4c303d 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -36,8 +36,12 @@ class mip_solver_settings_t { /** * @brief Set the callback for the user solution + * + * @param[in] callback - Callback handler for user solutions. + * @param[in] user_data - Pointer to user-defined data forwarded to the callback. */ - void set_mip_callback(internals::base_solution_callback_t* callback = nullptr); + void set_mip_callback(internals::base_solution_callback_t* callback = nullptr, + void* user_data = nullptr); /** * @brief Add an primal solution. diff --git a/cpp/include/cuopt/linear_programming/solver_settings.hpp b/cpp/include/cuopt/linear_programming/solver_settings.hpp index 180293254..dd910a8f4 100644 --- a/cpp/include/cuopt/linear_programming/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -81,7 +81,8 @@ class solver_settings_t { void add_initial_mip_solution(const f_t* initial_solution, i_t size, rmm::cuda_stream_view stream = rmm::cuda_stream_default); - void set_mip_callback(internals::base_solution_callback_t* callback = nullptr); + void set_mip_callback(internals::base_solution_callback_t* callback = nullptr, + void* user_data = nullptr); const pdlp_warm_start_data_view_t& get_pdlp_warm_start_data_view() const noexcept; const std::vector get_mip_callbacks() const; diff --git a/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp b/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp index f0cd74c24..a2c70d840 100644 --- a/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -38,15 +38,21 @@ class default_get_solution_callback_t : public get_solution_callback_t { } } - void get_solution(void* data, void* objective_value) override + void get_solution(void* data, void* objective_value, void* user_data) override { PyObject* numba_matrix = get_numba_matrix(data, n_variables); PyObject* numpy_array = get_numba_matrix(objective_value, 1); - PyObject* res = - PyObject_CallMethod(this->pyCallbackClass, "get_solution", "(OO)", numba_matrix, numpy_array); + PyObject* py_user_data = user_data == nullptr ? Py_None : static_cast(user_data); + PyObject* res = PyObject_CallMethod( + this->pyCallbackClass, "get_solution", "(OOO)", numba_matrix, numpy_array, py_user_data); + if (res == nullptr && PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + res = PyObject_CallMethod( + this->pyCallbackClass, "get_solution", "(OO)", numba_matrix, numpy_array); + } Py_DECREF(numba_matrix); Py_DECREF(numpy_array); - Py_DECREF(res); + if (res != nullptr) { Py_DECREF(res); } } PyObject* pyCallbackClass; @@ -75,15 +81,21 @@ class default_set_solution_callback_t : public set_solution_callback_t { } } - void set_solution(void* data, void* objective_value) override + void set_solution(void* data, void* objective_value, void* user_data) override { PyObject* numba_matrix = get_numba_matrix(data, n_variables); PyObject* numpy_array = get_numba_matrix(objective_value, 1); - PyObject* res = - PyObject_CallMethod(this->pyCallbackClass, "set_solution", "(OO)", numba_matrix, numpy_array); + PyObject* py_user_data = user_data == nullptr ? Py_None : static_cast(user_data); + PyObject* res = PyObject_CallMethod( + this->pyCallbackClass, "set_solution", "(OOO)", numba_matrix, numpy_array, py_user_data); + if (res == nullptr && PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + res = PyObject_CallMethod( + this->pyCallbackClass, "set_solution", "(OO)", numba_matrix, numpy_array); + } Py_DECREF(numba_matrix); Py_DECREF(numpy_array); - Py_DECREF(res); + if (res != nullptr) { Py_DECREF(res); } } PyObject* pyCallbackClass; diff --git a/cpp/include/cuopt/linear_programming/utilities/internals.hpp b/cpp/include/cuopt/linear_programming/utilities/internals.hpp index 90d856b23..fac6397de 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -31,16 +31,20 @@ class base_solution_callback_t : public Callback { this->n_variables = n_variables_; } + void set_user_data(void* user_data_) { user_data = user_data_; } + void* get_user_data() const { return user_data; } + virtual base_solution_callback_type get_type() const = 0; protected: bool isFloat = true; size_t n_variables = 0; + void* user_data = nullptr; }; class get_solution_callback_t : public base_solution_callback_t { public: - virtual void get_solution(void* data, void* objective_value) = 0; + virtual void get_solution(void* data, void* objective_value, void* user_data) = 0; base_solution_callback_type get_type() const override { return base_solution_callback_type::GET_SOLUTION; @@ -49,7 +53,7 @@ class get_solution_callback_t : public base_solution_callback_t { class set_solution_callback_t : public base_solution_callback_t { public: - virtual void set_solution(void* data, void* objective_value) = 0; + virtual void set_solution(void* data, void* objective_value, void* user_data) = 0; base_solution_callback_type get_type() const override { return base_solution_callback_type::SET_SOLUTION; diff --git a/cpp/src/linear_programming/cuopt_c.cpp b/cpp/src/linear_programming/cuopt_c.cpp index 0772dd14b..468c97335 100644 --- a/cpp/src/linear_programming/cuopt_c.cpp +++ b/cpp/src/linear_programming/cuopt_c.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -20,6 +20,7 @@ #include #include #include +#include using namespace cuopt::mps_parser; using namespace cuopt::linear_programming; @@ -49,6 +50,50 @@ struct solution_and_stream_view_t { rmm::cuda_stream_view stream_view; }; +class c_get_solution_callback_t : public cuopt::internals::get_solution_callback_t { + public: + explicit c_get_solution_callback_t(cuOptMipGetSolutionCallback callback) : callback_(callback) {} + + void get_solution(void* data, void* objective_value, void* user_data) override + { + if (callback_ == nullptr) { return; } + callback_(static_cast(data), + static_cast(objective_value), + user_data); + } + + private: + cuOptMipGetSolutionCallback callback_; +}; + +class c_set_solution_callback_t : public cuopt::internals::set_solution_callback_t { + public: + explicit c_set_solution_callback_t(cuOptMipSetSolutionCallback callback) : callback_(callback) {} + + void set_solution(void* data, void* objective_value, void* user_data) override + { + if (callback_ == nullptr) { return; } + callback_( + static_cast(data), static_cast(objective_value), user_data); + } + + private: + cuOptMipSetSolutionCallback callback_; +}; + +// Owns solver settings and C callback wrappers for C API lifetime. +struct solver_settings_handle_t { + solver_settings_handle_t() : settings(new solver_settings_t()) {} + ~solver_settings_handle_t() { delete settings; } + solver_settings_t* settings; + std::vector> callbacks; +}; + +solver_settings_handle_t* get_settings_handle(cuOptSolverSettings settings) +{ + return static_cast(settings); +} + int8_t cuOptGetFloatSize() { return sizeof(cuopt_float_t); } int8_t cuOptGetIntSize() { return sizeof(cuopt_int_t); } @@ -574,16 +619,15 @@ cuopt_int_t cuOptGetVariableTypes(cuOptOptimizationProblem problem, char* variab cuopt_int_t cuOptCreateSolverSettings(cuOptSolverSettings* settings_ptr) { if (settings_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } - solver_settings_t* settings = - new solver_settings_t(); - *settings_ptr = static_cast(settings); + solver_settings_handle_t* settings_handle = new solver_settings_handle_t(); + *settings_ptr = static_cast(settings_handle); return CUOPT_SUCCESS; } void cuOptDestroySolverSettings(cuOptSolverSettings* settings_ptr) { if (settings_ptr == nullptr) { return; } - delete static_cast*>(*settings_ptr); + delete get_settings_handle(*settings_ptr); *settings_ptr = nullptr; } @@ -595,7 +639,7 @@ cuopt_int_t cuOptSetParameter(cuOptSolverSettings settings, if (parameter_name == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (parameter_value == nullptr) { return CUOPT_INVALID_ARGUMENT; } solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; try { solver_settings->set_parameter_from_string(parameter_name, parameter_value); } catch (const std::exception& e) { @@ -614,7 +658,7 @@ cuopt_int_t cuOptGetParameter(cuOptSolverSettings settings, if (parameter_value == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (parameter_value_size <= 0) { return CUOPT_INVALID_ARGUMENT; } solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; try { std::string parameter_value_str = solver_settings->get_parameter_as_string(parameter_name); std::snprintf(parameter_value, parameter_value_size, "%s", parameter_value_str.c_str()); @@ -631,7 +675,7 @@ cuopt_int_t cuOptSetIntegerParameter(cuOptSolverSettings settings, if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (parameter_name == nullptr) { return CUOPT_INVALID_ARGUMENT; } solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; try { solver_settings->set_parameter(parameter_name, parameter_value); } catch (const std::invalid_argument& e) { @@ -656,7 +700,7 @@ cuopt_int_t cuOptGetIntegerParameter(cuOptSolverSettings settings, if (parameter_name == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (parameter_value_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; try { *parameter_value_ptr = solver_settings->get_parameter(parameter_name); } catch (const std::invalid_argument& e) { @@ -680,7 +724,7 @@ cuopt_int_t cuOptSetFloatParameter(cuOptSolverSettings settings, if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (parameter_name == nullptr) { return CUOPT_INVALID_ARGUMENT; } solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; try { solver_settings->set_parameter(parameter_name, parameter_value); } catch (const std::exception& e) { @@ -697,7 +741,7 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings, if (parameter_name == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (parameter_value_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; try { *parameter_value_ptr = solver_settings->get_parameter(parameter_name); } catch (const std::exception& e) { @@ -706,6 +750,32 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings, return CUOPT_SUCCESS; } +cuopt_int_t cuOptSetMipGetSolutionCallback(cuOptSolverSettings settings, + cuOptMipGetSolutionCallback callback, + void* user_data) +{ + if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (callback == nullptr) { return CUOPT_INVALID_ARGUMENT; } + solver_settings_handle_t* settings_handle = get_settings_handle(settings); + auto callback_wrapper = std::make_unique(callback); + settings_handle->settings->set_mip_callback(callback_wrapper.get(), user_data); + settings_handle->callbacks.push_back(std::move(callback_wrapper)); + return CUOPT_SUCCESS; +} + +cuopt_int_t cuOptSetMipSetSolutionCallback(cuOptSolverSettings settings, + cuOptMipSetSolutionCallback callback, + void* user_data) +{ + if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } + if (callback == nullptr) { return CUOPT_INVALID_ARGUMENT; } + solver_settings_handle_t* settings_handle = get_settings_handle(settings); + auto callback_wrapper = std::make_unique(callback); + settings_handle->settings->set_mip_callback(callback_wrapper.get(), user_data); + settings_handle->callbacks.push_back(std::move(callback_wrapper)); + return CUOPT_SUCCESS; +} + cuopt_int_t cuOptIsMIP(cuOptOptimizationProblem problem, cuopt_int_t* is_mip_ptr) { if (problem == nullptr) { return CUOPT_INVALID_ARGUMENT; } @@ -733,7 +803,7 @@ cuopt_int_t cuOptSolve(cuOptOptimizationProblem problem, if (problem_and_stream_view->op_problem->get_problem_category() == problem_category_t::MIP || problem_and_stream_view->op_problem->get_problem_category() == problem_category_t::IP) { solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; mip_solver_settings_t& mip_settings = solver_settings->get_mip_settings(); optimization_problem_t* op_problem = @@ -750,7 +820,7 @@ cuopt_int_t cuOptSolve(cuOptOptimizationProblem problem, solution_and_stream_view->mip_solution_ptr->get_error_status().get_error_type()); } else { solver_settings_t* solver_settings = - static_cast*>(settings); + get_settings_handle(settings)->settings; pdlp_solver_settings_t& pdlp_settings = solver_settings->get_pdlp_settings(); optimization_problem_t* op_problem = diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 4e3dc6465..94ef3d61b 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -382,9 +382,10 @@ void solver_settings_t::add_initial_mip_solution(const f_t* solution, } template -void solver_settings_t::set_mip_callback(internals::base_solution_callback_t* callback) +void solver_settings_t::set_mip_callback(internals::base_solution_callback_t* callback, + void* user_data) { - mip_settings.set_mip_callback(callback); + mip_settings.set_mip_callback(callback, user_data); } template diff --git a/cpp/src/mip/diversity/population.cu b/cpp/src/mip/diversity/population.cu index 766ed09cb..f343320cc 100644 --- a/cpp/src/mip/diversity/population.cu +++ b/cpp/src/mip/diversity/population.cu @@ -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 */ @@ -299,7 +299,8 @@ void population_t::run_solution_callbacks(solution_t& sol) temp_sol.problem_ptr->get_user_obj_from_solver_obj(temp_sol.get_objective()); user_objective_vec.set_element_async(0, user_objective, temp_sol.handle_ptr->get_stream()); CUOPT_LOG_DEBUG("Returning incumbent solution with objective %g", user_objective); - get_sol_callback->get_solution(temp_sol.assignment.data(), user_objective_vec.data()); + get_sol_callback->get_solution( + temp_sol.assignment.data(), user_objective_vec.data(), get_sol_callback->get_user_data()); } } // save the best objective here, because we might not have been able to return the solution to @@ -320,7 +321,9 @@ void population_t::run_solution_callbacks(solution_t& sol) auto inf = std::numeric_limits::infinity(); d_outside_sol_objective.set_value_async(inf, sol.handle_ptr->get_stream()); sol.handle_ptr->sync_stream(); - set_sol_callback->set_solution(incumbent_assignment.data(), d_outside_sol_objective.data()); + set_sol_callback->set_solution(incumbent_assignment.data(), + d_outside_sol_objective.data(), + set_sol_callback->get_user_data()); f_t outside_sol_objective = d_outside_sol_objective.value(sol.handle_ptr->get_stream()); // The callback might be called without setting any valid solution or objective which triggers diff --git a/cpp/src/mip/solver_settings.cu b/cpp/src/mip/solver_settings.cu index 205d4fe68..f69b45575 100644 --- a/cpp/src/mip/solver_settings.cu +++ b/cpp/src/mip/solver_settings.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -25,8 +25,10 @@ void mip_solver_settings_t::add_initial_solution(const f_t* initial_so template void mip_solver_settings_t::set_mip_callback( - internals::base_solution_callback_t* callback) + internals::base_solution_callback_t* callback, void* user_data) { + if (callback == nullptr) { return; } + callback->set_user_data(user_data); mip_callbacks_.push_back(callback); } diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_test.c b/cpp/tests/linear_programming/c_api_tests/c_api_test.c index 52be9e16f..7cff7e343 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_test.c +++ b/cpp/tests/linear_programming/c_api_tests/c_api_test.c @@ -9,6 +9,7 @@ #include +#include #include #include @@ -131,6 +132,169 @@ cuopt_int_t test_bad_parameter_name() { return status; } +typedef struct mip_callback_context_t { + cuopt_int_t n_variables; + int get_calls; + int set_calls; + int error; + cuopt_float_t last_objective; + cuopt_float_t* last_solution; +} mip_callback_context_t; + +static void mip_get_solution_callback(const cuopt_float_t* solution, + const cuopt_float_t* objective_value, + void* user_data) +{ + mip_callback_context_t* context = (mip_callback_context_t*)user_data; + if (context == NULL) { return; } + context->get_calls += 1; + if (context->last_solution == NULL) { + context->last_solution = + (cuopt_float_t*)malloc(context->n_variables * sizeof(cuopt_float_t)); + if (context->last_solution == NULL) { + context->error = 1; + return; + } + } + if (cudaMemcpy(context->last_solution, + solution, + context->n_variables * sizeof(cuopt_float_t), + cudaMemcpyDeviceToHost) != cudaSuccess) { + context->error = 1; + return; + } + if (cudaMemcpy(&context->last_objective, + objective_value, + sizeof(cuopt_float_t), + cudaMemcpyDeviceToHost) != cudaSuccess) { + context->error = 1; + } +} + +static void mip_set_solution_callback(cuopt_float_t* solution, + cuopt_float_t* objective_value, + void* user_data) +{ + mip_callback_context_t* context = (mip_callback_context_t*)user_data; + if (context == NULL) { return; } + context->set_calls += 1; + if (context->last_solution == NULL) { return; } + if (cudaMemcpy(solution, + context->last_solution, + context->n_variables * sizeof(cuopt_float_t), + cudaMemcpyHostToDevice) != cudaSuccess) { + context->error = 1; + return; + } + if (cudaMemcpy(objective_value, + &context->last_objective, + sizeof(cuopt_float_t), + cudaMemcpyHostToDevice) != cudaSuccess) { + context->error = 1; + } +} + +cuopt_int_t test_mip_callbacks() +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + mip_callback_context_t context = {0}; + +#define NUM_ITEMS 8 +#define NUM_CONSTRAINTS 1 + cuopt_int_t num_items = NUM_ITEMS; + cuopt_float_t max_weight = 102; + cuopt_float_t value[] = {15, 100, 90, 60, 40, 15, 10, 1}; + cuopt_float_t weight[] = {2, 20, 20, 30, 40, 30, 60, 10}; + + cuopt_int_t num_variables = NUM_ITEMS; + cuopt_int_t num_constraints = NUM_CONSTRAINTS; + + cuopt_int_t row_offsets[] = {0, NUM_ITEMS}; + cuopt_int_t column_indices[NUM_ITEMS]; + + cuopt_float_t rhs[] = {max_weight}; + char constraint_sense[] = {CUOPT_LESS_THAN}; + cuopt_float_t lower_bounds[NUM_ITEMS]; + cuopt_float_t upper_bounds[NUM_ITEMS]; + char variable_types[NUM_ITEMS]; + cuopt_int_t status; + + for (cuopt_int_t j = 0; j < NUM_ITEMS; j++) { + column_indices[j] = j; + } + + for (cuopt_int_t j = 0; j < NUM_ITEMS; j++) { + variable_types[j] = CUOPT_INTEGER; + lower_bounds[j] = 0; + upper_bounds[j] = 1; + } + + status = cuOptCreateProblem(num_constraints, + num_variables, + CUOPT_MAXIMIZE, + 0, + value, + row_offsets, + column_indices, + weight, + constraint_sense, + rhs, + lower_bounds, + upper_bounds, + variable_types, + &problem); + if (status != CUOPT_SUCCESS) { + printf("Error creating optimization problem\n"); + goto DONE; + } + + status = cuOptCreateSolverSettings(&settings); + if (status != CUOPT_SUCCESS) { + printf("Error creating solver settings\n"); + goto DONE; + } + + context.n_variables = num_variables; + status = cuOptSetMipGetSolutionCallback(settings, mip_get_solution_callback, &context); + if (status != CUOPT_SUCCESS) { + printf("Error setting get-solution callback\n"); + goto DONE; + } + + status = cuOptSetMipSetSolutionCallback(settings, mip_set_solution_callback, &context); + if (status != CUOPT_SUCCESS) { + printf("Error setting set-solution callback\n"); + goto DONE; + } + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving problem\n"); + goto DONE; + } + + if (context.error != 0) { + printf("Error in callback data transfer\n"); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + + if (context.get_calls < 1 || context.set_calls < 1) { + printf("Expected callbacks to be called at least once\n"); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + +DONE: + if (context.last_solution != NULL) { free(context.last_solution); } + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + return status; +} + cuopt_int_t burglar_problem() { cuOptOptimizationProblem problem = NULL; diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp index af1295298..a5405198a 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -88,6 +88,8 @@ TEST(c_api, solve_time_bb_preemption) TEST(c_api, bad_parameter_name) { EXPECT_EQ(test_bad_parameter_name(), CUOPT_INVALID_ARGUMENT); } +TEST(c_api, mip_callbacks) { EXPECT_EQ(test_mip_callbacks(), CUOPT_SUCCESS); } + TEST(c_api, burglar) { EXPECT_EQ(burglar_problem(), CUOPT_SUCCESS); } TEST(c_api, test_missing_file) { EXPECT_EQ(test_missing_file(), CUOPT_MPS_FILE_ERROR); } diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.h b/cpp/tests/linear_programming/c_api_tests/c_api_tests.h index 5726c3a99..808179756 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.h +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.h @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -28,6 +28,7 @@ cuopt_int_t solve_mps_file(const char* filename, cuopt_int_t test_missing_file(); cuopt_int_t test_infeasible_problem(); cuopt_int_t test_bad_parameter_name(); +cuopt_int_t test_mip_callbacks(); cuopt_int_t test_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip); cuopt_int_t test_quadratic_problem(cuopt_int_t* termination_status_ptr, diff --git a/cpp/tests/mip/incumbent_callback_test.cu b/cpp/tests/mip/incumbent_callback_test.cu index 6d6792df2..945a702d4 100644 --- a/cpp/tests/mip/incumbent_callback_test.cu +++ b/cpp/tests/mip/incumbent_callback_test.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -40,8 +40,9 @@ class test_set_solution_callback_t : public cuopt::internals::set_solution_callb { } // This will check that the we are able to recompute our own solution - void set_solution(void* data, void* cost) override + void set_solution(void* data, void* cost, void* user_data) override { + (void)user_data; n_calls++; rmm::cuda_stream_view stream{}; auto assignment = static_cast(data); @@ -64,8 +65,9 @@ class test_get_solution_callback_t : public cuopt::internals::get_solution_callb : solutions(solutions_in), n_calls(0), n_variables(n_variables_) { } - void get_solution(void* data, void* cost) override + void get_solution(void* data, void* cost, void* user_data) override { + (void)user_data; n_calls++; rmm::cuda_stream_view stream{}; rmm::device_uvector assignment(n_variables, stream); diff --git a/python/cuopt/cuopt/linear_programming/internals/internals.pyx b/python/cuopt/cuopt/linear_programming/internals/internals.pyx index 0e4342fe1..695465fbb 100644 --- a/python/cuopt/cuopt/linear_programming/internals/internals.pyx +++ b/python/cuopt/cuopt/linear_programming/internals/internals.pyx @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 # cython: profile=False @@ -8,32 +8,47 @@ from libc.stdint cimport uintptr_t +from cpython.ref cimport PyObject import numpy as np from numba.cuda.api import from_cuda_array_interface -cdef extern from "Python.h": - cdef cppclass PyObject - - cdef extern from "cuopt/linear_programming/utilities/callbacks_implems.hpp" namespace "cuopt::internals": # noqa cdef cppclass Callback: pass cdef cppclass default_get_solution_callback_t(Callback): void setup() except + - void get_solution(void* data, void* objective_value) except + + void get_solution(void* data, void* objective_value, void* user_data) except + PyObject* pyCallbackClass cdef cppclass default_set_solution_callback_t(Callback): void setup() except + - void set_solution(void* data, void* objective_value) except + + void set_solution(void* data, void* objective_value, void* user_data) except + PyObject* pyCallbackClass cdef class PyCallback: + cdef object _user_data + + def __init__(self): + self._user_data = None + + property user_data: + def __get__(self): + return self._user_data + def __set__(self, value): + self._user_data = value + + cpdef uintptr_t get_user_data_ptr(self): + cdef PyObject* ptr + if self._user_data is None: + return 0 + ptr = self._user_data + return ptr + def get_numba_matrix(self, data, shape, typestr): sizeofType = 4 if typestr == "float32" else 8 @@ -68,6 +83,7 @@ cdef class GetSolutionCallback(PyCallback): cdef default_get_solution_callback_t native_callback def __init__(self): + super().__init__() self.native_callback.pyCallbackClass = self def get_native_callback(self): @@ -79,6 +95,7 @@ cdef class SetSolutionCallback(PyCallback): cdef default_set_solution_callback_t native_callback def __init__(self): + super().__init__() self.native_callback.pyCallbackClass = self def get_native_callback(self): diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.pxd b/python/cuopt/cuopt/linear_programming/solver/solver.pxd index c140e3d0c..6688e5166 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.pxd +++ b/python/cuopt/cuopt/linear_programming/solver/solver.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 @@ -78,7 +78,8 @@ cdef extern from "cuopt/linear_programming/solver_settings.hpp" namespace "cuopt i_t size ) except + void set_mip_callback( - base_solution_callback_t* callback + base_solution_callback_t* callback, + void* user_data ) except + diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 1991af0d6..8c1c8fdc3 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 @@ -169,6 +169,7 @@ cdef set_solver_setting( cdef uintptr_t c_last_restart_duality_gap_primal_solution cdef uintptr_t c_last_restart_duality_gap_dual_solution cdef uintptr_t callback_ptr = 0 + cdef uintptr_t callback_user_data = 0 if mip: if data_model_obj is not None and data_model_obj.get_initial_primal_solution().shape[0] != 0: # noqa c_solver_settings.add_initial_mip_solution( @@ -186,9 +187,15 @@ cdef set_solver_setting( for callback in callbacks: if callback: callback_ptr = callback.get_native_callback() + callback_user_data = ( + callback.get_user_data_ptr() + if hasattr(callback, "get_user_data_ptr") + else 0 + ) c_solver_settings.set_mip_callback( - callback_ptr + callback_ptr, + callback_user_data ) else: if data_model_obj is not None and data_model_obj.get_initial_primal_solution().shape[0] != 0: # noqa diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 85a944e2e..41d086752 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum, auto @@ -245,7 +245,7 @@ def set_pdlp_warm_start_data(self, pdlp_warm_start_data): """ self.pdlp_warm_start_data = pdlp_warm_start_data - def set_mip_callback(self, callback): + def set_mip_callback(self, callback, user_data=None): """ Note: Only supported for MILP @@ -256,6 +256,8 @@ def set_mip_callback(self, callback): callback : class for function callback Callback class that inherits from GetSolutionCallback or SetSolutionCallback. + user_data : object, optional + User context passed to the callback. Examples -------- @@ -298,6 +300,8 @@ def set_mip_callback(self, callback): >>> settings.set_mip_callback(get_callback) >>> settings.set_mip_callback(set_callback) """ + if callback is not None: + callback.user_data = user_data self.mip_callbacks.append(callback) def get_mip_callbacks(self):