From dab644e9aa3b0714241478f8e2fdc09b29958800 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Fri, 10 Jul 2026 16:42:05 -0400 Subject: [PATCH] change semantics of problem construction in cuOptReadProblem since the mps data model is constructed internally, use move semantics when building a CPU problem to avoid a copy --- cpp/cuopt_cli.cpp | 9 +- .../cpu_optimization_problem.hpp | 12 ++ .../optimization_problem_utils.hpp | 21 ++++ cpp/src/pdlp/cpu_optimization_problem.cpp | 111 ++++++++++++++++-- cpp/src/pdlp/cuopt_c.cpp | 8 +- .../unit_tests/solution_interface_test.cu | 91 ++++++++++++++ 6 files changed, 234 insertions(+), 18 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index bf16e9e9da..59a45a8c07 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -138,8 +138,11 @@ int run_single_file(const std::string& file_path, std::make_unique>(); } - cuopt::mathematical_optimization::populate_from_mps_data_model(problem_interface.get(), - mps_data_model); + // adopt_from_mps_data_model moves the parsed storage into a HOST-backed problem and falls back to + // a copy/H2D populate for a DEVICE-backed problem, so this single call is correct for either + // backend. Nothing reads mps_data_model afterward (names come from problem_interface below). + cuopt::mathematical_optimization::adopt_from_mps_data_model(problem_interface.get(), + std::move(mps_data_model)); const bool is_mip = (problem_interface->get_problem_category() == cuopt::mathematical_optimization::problem_category_t::MIP || @@ -152,7 +155,7 @@ int run_single_file(const std::string& file_path, initial_solution_file.empty() ? std::vector() : cuopt::mathematical_optimization::solution_reader_t::get_variable_values_from_sol_file( - initial_solution_file, mps_data_model.get_variable_names()); + initial_solution_file, problem_interface->get_variable_names()); if (is_mip) { auto& mip_settings = settings.get_mip_settings(); diff --git a/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp b/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp index fd4c50fa75..3762701151 100644 --- a/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp +++ b/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp @@ -18,6 +18,11 @@ #include #include +namespace cuopt::mathematical_optimization::io { +template +class mps_data_model_t; +} // namespace cuopt::mathematical_optimization::io + namespace cuopt::mathematical_optimization { // Forward declarations @@ -77,6 +82,13 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t& variable_names) override; void set_row_names(const std::vector& row_names) override; + /** + * @brief Transfer parsed MPS/QPS storage into this CPU problem without copying array/string data. + * + * The model is left in a moved-from state and must not be used afterward. + */ + void adopt_from_mps_data_model(io::mps_data_model_t&& data_model); + // Device getters - throw exceptions (not supported for CPU implementation) i_t get_n_variables() const override; i_t get_n_constraints() const override; diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp index 7d511ef6f9..dab2ce9454 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp @@ -136,6 +136,27 @@ void populate_from_mps_data_model(optimization_problem_interface_t* pr } } +/** + * @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays. + * + * For GPU-backed problems this falls back to populate_from_mps_data_model (copy/H2D path). + * + * @tparam i_t Integer type for indices + * @tparam f_t Floating point type for values + * @param[out] problem The optimization problem interface to populate + * @param[in] data_model Parsed model; moved-from on return for CPU adopt + */ +template +void adopt_from_mps_data_model(optimization_problem_interface_t* problem, + io::mps_data_model_t&& data_model) +{ + if (auto* cpu_problem = dynamic_cast*>(problem)) { + cpu_problem->adopt_from_mps_data_model(std::move(data_model)); + return; + } + populate_from_mps_data_model(problem, data_model); +} + /** * @brief Helper function to populate optimization_problem_interface_t from data_model_view_t * diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index 84f03b5e85..725424bf6b 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,26 @@ namespace cuopt::mathematical_optimization { +namespace { + +// Classify a problem as LP / MIP / IP from its (enum) variable types. Single source of truth +// shared by set_variable_types() and adopt_from_mps_data_model() so the detection rule lives in +// one place. Empty types (no variables declared) classify as LP, matching the populate path where +// set_variable_types() is skipped and the category keeps its LP default. +problem_category_t problem_category_from_variable_types(const std::vector& variable_types) +{ + if (variable_types.empty()) { return problem_category_t::LP; } + const std::size_t n_discrete = static_cast( + std::count_if(variable_types.begin(), variable_types.end(), [](var_t v) { + return v == var_t::INTEGER || v == var_t::SEMI_CONTINUOUS; + })); + if (n_discrete == variable_types.size()) { return problem_category_t::IP; } + if (n_discrete > 0) { return problem_category_t::MIP; } + return problem_category_t::LP; +} + +} // namespace + // ============================================================================== // Constructor // ============================================================================== @@ -210,18 +231,7 @@ void cpu_optimization_problem_t::set_variable_types(const var_t* varia variable_types_.resize(size); std::copy(variable_types, variable_types + size, variable_types_.begin()); - // Auto-detect problem category based on variable types (matching original optimization_problem_t) - i_t n_discrete = std::count_if(variable_types_.begin(), variable_types_.end(), [](auto val) { - return val == var_t::INTEGER || val == var_t::SEMI_CONTINUOUS; - }); - // By default it is LP - if (n_discrete == size) { - problem_category_ = problem_category_t::IP; - } else if (n_discrete > 0) { - problem_category_ = problem_category_t::MIP; - } else { - problem_category_ = problem_category_t::LP; - } + problem_category_ = problem_category_from_variable_types(variable_types_); } template @@ -1095,6 +1105,83 @@ void cpu_optimization_problem_t::copy_variable_types_to_host(var_t* ou std::copy(variable_types_.begin(), variable_types_.begin() + size, output); } +// ============================================================================== +// adopt_from_mps_data_model +// ============================================================================== + +namespace { + +template +void move_quadratic_constraints_from_model( + cpu_optimization_problem_t& problem, + std::vector::quadratic_constraint_t>& model_constraints) +{ + using model_qc_t = typename io::mps_data_model_t::quadratic_constraint_t; + std::vector::quadratic_constraint_t> converted; + converted.reserve(model_constraints.size()); + for (model_qc_t& qc : model_constraints) { + converted.push_back({qc.constraint_row_index, + std::move(qc.constraint_row_name), + qc.constraint_row_type, + std::move(qc.linear_values), + std::move(qc.linear_indices), + qc.rhs_value, + std::move(qc.rows), + std::move(qc.cols), + std::move(qc.vals)}); + } + model_constraints.clear(); + problem.set_quadratic_constraints(std::move(converted)); +} + +} // namespace + +template +void cpu_optimization_problem_t::adopt_from_mps_data_model( + io::mps_data_model_t&& model) +{ + maximize_ = model.maximize_; + n_vars_ = model.n_vars_; + n_constraints_ = model.n_constraints_; + objective_scaling_factor_ = model.objective_scaling_factor_; + objective_offset_ = model.objective_offset_; + + A_ = std::move(model.A_); + A_indices_ = std::move(model.A_indices_); + A_offsets_ = std::move(model.A_offsets_); + b_ = std::move(model.b_); + c_ = std::move(model.c_); + constraint_lower_bounds_ = std::move(model.constraint_lower_bounds_); + constraint_upper_bounds_ = std::move(model.constraint_upper_bounds_); + row_types_ = std::move(model.row_types_); + variable_lower_bounds_ = std::move(model.variable_lower_bounds_); + variable_upper_bounds_ = std::move(model.variable_upper_bounds_); + + objective_name_ = std::move(model.objective_name_); + problem_name_ = std::move(model.problem_name_); + var_names_ = std::move(model.var_names_); + row_names_ = std::move(model.row_names_); + + Q_values_ = std::move(model.Q_objective_values_); + Q_indices_ = std::move(model.Q_objective_indices_); + Q_offsets_ = std::move(model.Q_objective_offsets_); + + variable_types_.resize(model.var_types_.size()); + for (size_t i = 0; i < model.var_types_.size(); ++i) { + variable_types_[i] = char_to_var_type(model.var_types_[i]); + } + problem_category_ = problem_category_from_variable_types(variable_types_); + + if (model.has_quadratic_constraints()) { + move_quadratic_constraints_from_model(*this, model.quadratic_constraints_); + } + + model.var_types_.clear(); + model.n_vars_ = 0; + model.n_constraints_ = 0; + model.nnz_ = 0; +} + // ============================================================================== // Template instantiations matching optimization_problem_t // ============================================================================== diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 963bd6ada7..9827b36b79 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -217,9 +217,11 @@ cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* pro } } - // Populate interface directly from MPS data model (avoids temporary GPU allocation) - cuopt::mathematical_optimization::populate_from_mps_data_model(problem_and_stream->get_problem(), - *mps_data_model_ptr); + // Transfer parsed payload into the problem. adopt_from_mps_data_model moves the model's storage + // into a HOST-backed problem and falls back to a copy/H2D populate for a DEVICE-backed problem, + // so the same call is correct regardless of the memory backend. + cuopt::mathematical_optimization::adopt_from_mps_data_model(problem_and_stream->get_problem(), + std::move(*mps_data_model_ptr)); *problem_ptr = static_cast(problem_and_stream); return CUOPT_SUCCESS; diff --git a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu index e55f4b99b6..0533dc394a 100644 --- a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu @@ -424,6 +424,97 @@ End EXPECT_NEAR(problem.get_quadratic_constraints()[0].rhs_value, 0.5, 1e-9); } +// Build one CPU problem by copying the model (populate) and another by moving an equal model +// (adopt), then assert the two are field-for-field identical. Guards the adopt path against +// silently dropping or mis-moving any member. +static void expect_adopt_matches_populate(const io::mps_data_model_t& model_src) +{ + io::mps_data_model_t model_for_copy = model_src; + io::mps_data_model_t model_for_move = model_src; + + cpu_optimization_problem_t copied; + populate_from_mps_data_model(&copied, model_for_copy); + + cpu_optimization_problem_t moved; + adopt_from_mps_data_model(&moved, std::move(model_for_move)); + + // Scalars + EXPECT_EQ(copied.get_n_variables(), moved.get_n_variables()); + EXPECT_EQ(copied.get_n_constraints(), moved.get_n_constraints()); + EXPECT_EQ(copied.get_nnz(), moved.get_nnz()); + EXPECT_EQ(copied.get_n_integers(), moved.get_n_integers()); + EXPECT_EQ(copied.get_problem_category(), moved.get_problem_category()); + EXPECT_EQ(copied.get_sense(), moved.get_sense()); + EXPECT_DOUBLE_EQ(copied.get_objective_offset(), moved.get_objective_offset()); + EXPECT_DOUBLE_EQ(copied.get_objective_scaling_factor(), moved.get_objective_scaling_factor()); + EXPECT_EQ(copied.has_quadratic_objective(), moved.has_quadratic_objective()); + EXPECT_EQ(copied.has_quadratic_constraints(), moved.has_quadratic_constraints()); + + // Numeric / char / string arrays + EXPECT_EQ(copied.get_constraint_matrix_values_host(), moved.get_constraint_matrix_values_host()); + EXPECT_EQ(copied.get_constraint_matrix_indices_host(), + moved.get_constraint_matrix_indices_host()); + EXPECT_EQ(copied.get_constraint_matrix_offsets_host(), + moved.get_constraint_matrix_offsets_host()); + EXPECT_EQ(copied.get_constraint_bounds_host(), moved.get_constraint_bounds_host()); + EXPECT_EQ(copied.get_constraint_lower_bounds_host(), moved.get_constraint_lower_bounds_host()); + EXPECT_EQ(copied.get_constraint_upper_bounds_host(), moved.get_constraint_upper_bounds_host()); + EXPECT_EQ(copied.get_objective_coefficients_host(), moved.get_objective_coefficients_host()); + EXPECT_EQ(copied.get_variable_lower_bounds_host(), moved.get_variable_lower_bounds_host()); + EXPECT_EQ(copied.get_variable_upper_bounds_host(), moved.get_variable_upper_bounds_host()); + EXPECT_EQ(copied.get_row_types_host(), moved.get_row_types_host()); + EXPECT_EQ(copied.get_variable_types_host(), moved.get_variable_types_host()); + EXPECT_EQ(copied.get_variable_names(), moved.get_variable_names()); + EXPECT_EQ(copied.get_row_names(), moved.get_row_names()); + + // Quadratic objective (CSR) + EXPECT_EQ(copied.get_quadratic_objective_values(), moved.get_quadratic_objective_values()); + EXPECT_EQ(copied.get_quadratic_objective_indices(), moved.get_quadratic_objective_indices()); + EXPECT_EQ(copied.get_quadratic_objective_offsets(), moved.get_quadratic_objective_offsets()); + + // Quadratic constraints (compare size + per-row key fields) + const auto& qc_copied = copied.get_quadratic_constraints(); + const auto& qc_moved = moved.get_quadratic_constraints(); + ASSERT_EQ(qc_copied.size(), qc_moved.size()); + for (size_t i = 0; i < qc_copied.size(); ++i) { + EXPECT_EQ(qc_copied[i].constraint_row_name, qc_moved[i].constraint_row_name); + EXPECT_EQ(qc_copied[i].constraint_row_type, qc_moved[i].constraint_row_type); + EXPECT_DOUBLE_EQ(qc_copied[i].rhs_value, qc_moved[i].rhs_value); + EXPECT_EQ(qc_copied[i].linear_values, qc_moved[i].linear_values); + EXPECT_EQ(qc_copied[i].linear_indices, qc_moved[i].linear_indices); + EXPECT_EQ(qc_copied[i].rows, qc_moved[i].rows); + EXPECT_EQ(qc_copied[i].cols, qc_moved[i].cols); + EXPECT_EQ(qc_copied[i].vals, qc_moved[i].vals); + } + + // The moved-from model must be left empty (adopt consumed it). + EXPECT_EQ(model_for_move.get_n_variables(), 0); + EXPECT_EQ(model_for_move.get_n_constraints(), 0); +} + +// adopt_from_mps_data_model must produce a problem identical to populate_from_mps_data_model. +TEST_F(SolutionInterfaceTest, adopt_matches_populate_lp_file) +{ + const auto model = cuopt::mathematical_optimization::io::read_mps(lp_file_); + expect_adopt_matches_populate(model); +} + +TEST_F(SolutionInterfaceTest, adopt_matches_populate_quadratic_constraints) +{ + const auto model = cuopt::mathematical_optimization::io::read_lp_from_string(R"LP( +Minimize + obj: x + y +Subject To + q0: [ 4 x * y ] <= 0.5 +Bounds + -1 <= x <= 1 + -1 <= y <= 1 +End +)LP"); + ASSERT_TRUE(model.has_quadratic_constraints()); + expect_adopt_matches_populate(model); +} + // ============================================================================= // Solution conversion tests (hand-constructed, known values) // =============================================================================