Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
68 changes: 58 additions & 10 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,25 @@ endif ()
find_package(OpenMP REQUIRED)
message(VERBOSE "cuOpt: OpenMP found in ${OpenMP_CXX_INCLUDE_DIRS}")

# MPS/QPS parser supports compressed inputs via bzip2 and zlib
# Resolve libgomp from the active C++ compiler, not FindOpenMP's generic -lgomp (which can
# resolve to an older system libgomp on Rocky/RHEL wheel builders). The fast MPS parser uses
# OpenMP 5.0 detached tasks (omp_fulfill_event); compile and link must use the same libgomp.
execute_process(
COMMAND ${CMAKE_CXX_COMPILER} -print-file-name=libgomp.so
OUTPUT_VARIABLE CUOPT_LIBGOMP_FILE
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if (NOT IS_ABSOLUTE "${CUOPT_LIBGOMP_FILE}")
message(FATAL_ERROR "Could not resolve libgomp from ${CMAKE_CXX_COMPILER}: '${CUOPT_LIBGOMP_FILE}'")
endif ()
get_filename_component(CUOPT_LIBGOMP_DIR "${CUOPT_LIBGOMP_FILE}" DIRECTORY)
message(STATUS "cuOpt: libgomp for OpenMP link = ${CUOPT_LIBGOMP_FILE}")
list(APPEND CUOPT_CXX_FLAGS -fopenmp)

# MPS/QPS parser supports compressed inputs via bzip2, zlib and lz4
option(CUOPT_PARSER_WITH_BZIP2 "Build MPS parser with bzip2 decompression" ON)
option(CUOPT_PARSER_WITH_ZLIB "Build MPS parser with zlib decompression" ON)
option(CUOPT_PARSER_WITH_LZ4 "Build experimental fast MPS parser with LZ4 decompression" ON)
if (CUOPT_PARSER_WITH_BZIP2)
find_package(BZip2 REQUIRED)
add_compile_definitions(MPS_PARSER_WITH_BZIP2)
Expand All @@ -213,6 +229,10 @@ if (CUOPT_PARSER_WITH_ZLIB)
find_package(ZLIB REQUIRED)
add_compile_definitions(MPS_PARSER_WITH_ZLIB)
endif ()
if (CUOPT_PARSER_WITH_LZ4)
# No headers or link target needed; the experimental reader loads one liblz4 symbol at runtime.
add_compile_definitions(MPS_PARSER_WITH_LZ4)
endif ()

# Debug options
if (CMAKE_BUILD_TYPE MATCHES Debug)
Expand Down Expand Up @@ -250,6 +270,20 @@ else ()
find_package(RAFT REQUIRED)
endif ()

rapids_cpm_find(simde 0.8.2
CPM_ARGS
GIT_REPOSITORY https://github.com/simd-everywhere/simde.git
GIT_TAG v0.8.2
GIT_SHALLOW TRUE
DOWNLOAD_ONLY TRUE
)

if (NOT TARGET simde::simde)
add_library(simde::simde INTERFACE IMPORTED GLOBAL)
set_target_properties(simde::simde
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${simde_SOURCE_DIR}")
endif ()

FetchContent_Declare(
papilo
GIT_REPOSITORY "https://github.com/scipopt/papilo.git"
Expand Down Expand Up @@ -436,16 +470,27 @@ if (BUILD_TESTS)
endif ()

set(CUOPT_SRC_FILES)
set(MPS_FAST_SRC_FILES)
add_subdirectory(src)
if (HOST_LINEINFO)
set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1")
set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1")
endif ()

# Needed for the fast MPS parser, available on all x86-64-v3 compliant x86 CPUs (essentially since Haswell ~2013)
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND
CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
set_property(SOURCE ${MPS_FAST_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
APPEND PROPERTY COMPILE_OPTIONS "-mbmi2;-mavx2;-msse4.2")
endif ()

# TODO: figure out a set of flags for ARM that fits the range of CPUs we wish to support (neoverse?)
# NEON should be universal on aarch64 and enough for our purposes (parsing) though

# Apply -UNDEBUG only to solver source files (not gRPC infrastructure).
# Must happen before gRPC files are appended to CUOPT_SRC_FILES.
# Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO).
if (DEFINE_ASSERT)
set_property(SOURCE ${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_SOURCE_DIR}
set_property(SOURCE ${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG")
endif ()

Expand All @@ -470,7 +515,7 @@ if (NOT SKIP_GRPC_BUILD)
# The conda-forge abseil shared library is built with NDEBUG and does not
# export that symbol (abseil-cpp#1624). Without this, Debug builds fail
# at runtime with "undefined symbol: absl::…::Mutex::Dtor".
set_property(SOURCE ${GRPC_INFRA_FILES} DIRECTORY ${CMAKE_SOURCE_DIR}
set_property(SOURCE ${GRPC_INFRA_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
APPEND PROPERTY COMPILE_OPTIONS "-DNDEBUG")
endif (NOT SKIP_GRPC_BUILD)

Expand All @@ -483,6 +528,7 @@ set_target_properties(cuopt
INSTALL_RPATH "\$ORIGIN"
INTERFACE_POSITION_INDEPENDENT_CODE ON
CXX_SCAN_FOR_MODULES OFF
LINKER_LANGUAGE CXX
)

target_compile_definitions(cuopt
Expand Down Expand Up @@ -552,8 +598,7 @@ add_dependencies(cuopt PSLP)
set(CUOPT_PRIVATE_CUDA_LIBS
CUDA::curand
CUDA::cusolver
TBB::tbb
OpenMP::OpenMP_CXX)
TBB::tbb)

list(PREPEND CUOPT_PRIVATE_CUDA_LIBS CUDA::cublasLt)

Expand Down Expand Up @@ -596,10 +641,17 @@ target_link_libraries(cuopt
${CUDSS_LIB_FILE}
PRIVATE
${CUOPT_PRIVATE_CUDA_LIBS}
simde::simde
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:protobuf::libprotobuf>
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:gRPC::grpc++>
)

# Force libgomp from the active C++ toolchain into libcuopt.so. OpenMP::OpenMP_CXX and/or
# -fopenmp alone can leave omp_fulfill_event undefined (CUDA-linked target + --as-needed) or
# resolve a trailing bare -lgomp to an older system libgomp at executable link time.
target_link_directories(cuopt PRIVATE ${CUOPT_LIBGOMP_DIR})
target_link_libraries(cuopt PRIVATE "-Wl,--no-as-needed" gomp "-Wl,--as-needed")


# ##################################################################################################
# - generate tests --------------------------------------------------------------------------------
Expand Down Expand Up @@ -737,7 +789,6 @@ if (NOT BUILD_LP_ONLY)
target_link_libraries(cuopt_cli
PUBLIC
cuopt
OpenMP::OpenMP_CXX
${CUDSS_LIBRARIES}
TBB::tbb
PRIVATE
Expand Down Expand Up @@ -779,7 +830,6 @@ if (BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY)
target_link_libraries(solve_MIP
PUBLIC
cuopt
OpenMP::OpenMP_CXX
PRIVATE
)
if (NOT DEFINED INSTALL_TARGET OR "${INSTALL_TARGET}" STREQUAL "")
Expand Down Expand Up @@ -809,7 +859,6 @@ if (BUILD_LP_BENCHMARKS)
target_link_libraries(solve_LP
PUBLIC
cuopt
OpenMP::OpenMP_CXX
PRIVATE
)
if (NOT DEFINED INSTALL_TARGET OR "${INSTALL_TARGET}" STREQUAL "")
Expand Down Expand Up @@ -862,7 +911,6 @@ if (NOT SKIP_GRPC_BUILD)
target_link_libraries(cuopt_grpc_server
PUBLIC
cuopt
OpenMP::OpenMP_CXX
PRIVATE
protobuf::libprotobuf
gRPC::grpc++
Expand Down
24 changes: 20 additions & 4 deletions cpp/cuopt_cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ inline cuopt::init_logger_t dummy_logger(
* .mps/.qps and their .gz/.bz2 variants → MPS parser;
* anything else is rejected.
* @param initial_solution_file Path to initial solution file in SOL format
* @param mps_reader MPS reader implementation selected by the CLI
* @param settings Merged solver settings (config file loaded in main, then CLI overrides applied)
*/
int run_single_file(const std::string& file_path,
const std::string& initial_solution_file,
bool solve_relaxation,
cuopt::linear_programming::io::mps_reader_type_t mps_reader,
cuopt::linear_programming::solver_settings_t<int, double>& settings)
{
cuopt::init_logger_t log(settings.get_parameter<std::string>(CUOPT_LOG_FILE),
Expand All @@ -108,7 +110,7 @@ int run_single_file(const std::string& file_path,
{
CUOPT_LOG_INFO("Reading file %s", base_filename.c_str());
try {
mps_data_model = cuopt::linear_programming::io::read<int, double>(file_path);
mps_data_model = cuopt::linear_programming::io::read<int, double>(file_path, mps_reader);
} catch (const std::logic_error& e) {
CUOPT_LOG_ERROR("Parser exception: %s", e.what());
parsing_failed = true;
Expand Down Expand Up @@ -284,8 +286,8 @@ int main(int argc, char* argv[])
program.add_argument("filename")
.help(
"input problem file; format dispatched by extension (case-insensitive). "
"Supported: .lp, .mps, .qps and their .gz / .bz2 compressed variants "
"(e.g. .lp.gz, .mps.bz2, .qps.gz)")
"Supported: .lp, .mps, .qps and their .gz / .bz2 / .lz4 compressed variants "
"(e.g. .lp.gz, .mps.bz2, .qps.lz4).")
.nargs(1)
.required();

Expand All @@ -303,6 +305,14 @@ int main(int argc, char* argv[])
.help("path to parameter config file (key = value format, supports all parameters)")
.default_value(std::string(""));

program.add_argument("--mps-reader")
.help(
"MPS reader implementation: default uses the production parser; experimental-fast uses the "
"experimental SIMD parser for free-format LP/MIP/QP/QCQP (SOCP) .mps/.qps files and their "
".gz/.bz2/.lz4 compressed variants")
.default_value(std::string("default"))
.choices("default", "experimental-fast");

program.add_argument("--dump-hyper-params")
.help("print hyper-parameters only in config file format and exit")
.default_value(false)
Expand Down Expand Up @@ -403,6 +413,12 @@ int main(int argc, char* argv[])
const auto initial_solution_file = program.get<std::string>("--initial-solution");
const auto solve_relaxation = program.get<bool>("--relaxation");
const auto params_file = program.get<std::string>("--params-file");
const auto mps_reader_arg = program.get<std::string>("--mps-reader");

auto mps_reader = cuopt::linear_programming::io::mps_reader_type_t::default_reader;
if (mps_reader_arg == "experimental-fast") {
mps_reader = cuopt::linear_programming::io::mps_reader_type_t::fast_experimental;
}

cuopt::linear_programming::solver_settings_t<int, double> settings;
try {
Expand Down Expand Up @@ -432,5 +448,5 @@ int main(int argc, char* argv[])
RAFT_CUDA_TRY(cudaSetDevice(0));
}

return run_single_file(file_name, initial_solution_file, solve_relaxation, settings);
return run_single_file(file_name, initial_solution_file, solve_relaxation, mps_reader, settings);
}
82 changes: 69 additions & 13 deletions cpp/include/cuopt/linear_programming/io/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,26 @@

#include <algorithm>
#include <cctype>
#include <cstring>
#include <stdexcept>
#include <string>
#include <string_view>

namespace cuopt::linear_programming::io {

/**
* @brief Selects which MPS reader implementation should be used by dispatching entry points.
*
* The experimental fast reader is intentionally opt-in. It supports the same free-format
* MPS/QPS scope as read_mps(): LP, MIP, QP (QUADOBJ/QMATRIX), and QCQP/SOCP (QCMATRIX).
*/
enum class mps_reader_type_t { default_reader, fast_experimental };
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* @brief Reads the equation from an MPS or QPS file.
*
* The input file can be a plain text file in MPS-/QPS-format or a compressed MPS/QPS
* file (.mps.gz or .mps.bz2).
* file (.mps.gz, .mps.bz2, or .mps.lz4).
*
* Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more
* details on both free and fixed MPS format.
Expand All @@ -32,8 +41,8 @@ namespace cuopt::linear_programming::io {
* - QMATRIX: Full symmetric quadratic objective matrix (alternative to QUADOBJ)
* - QCMATRIX: Symmetric quadratic terms for a named constraint row (QCQP)
*
* Note: Compressed MPS files .mps.gz, .mps.bz2 can only be read if the compression
* libraries zlib or libbzip2 are installed, respectively.
* Note: Compressed MPS files .mps.gz, .mps.bz2, and .mps.lz4 can only be read if
* zlib, libbzip2, or liblz4 are installed, respectively.
*
* @param[in] mps_file_path Path to MPS/QPSfile.
* @param[in] fixed_mps_format If MPS/QPS file should be parsed as fixed, false by default
Expand All @@ -43,6 +52,19 @@ template <typename i_t, typename f_t>
mps_data_model_t<i_t, f_t> read_mps(const std::string& mps_file_path,
bool fixed_mps_format = false);

/**
* @brief Reads an MPS/QPS problem with the experimental SIMD-optimized reader.
*
* Supports the same free-format LP/MIP/QP/QCQP (SOCP-relevant QCMATRIX) scope as read_mps().
* Fixed MPS format forcing is not supported. Accepts .mps/.qps and their .gz/.bz2/.lz4 variants
* (compression is detected from the file path, same as read_mps()).
*
* @param[in] mps_file_path Path to a raw or compressed .mps or .qps file.
* @return mps_data_model_t A fully formed LP/MIP/QP problem which represents the given file.
*/
template <typename i_t, typename f_t>
mps_data_model_t<i_t, f_t> read_mps_fast_experimental(const std::string& mps_file_path);

/**
* @brief Reads an MPS problem from in-memory file contents.
*
Expand Down Expand Up @@ -111,38 +133,72 @@ mps_data_model_t<i_t, f_t> read_lp_from_string(std::string_view lp_contents);
* @brief Reads an optimization problem from a file, dispatching on the file
* extension. Extension matching is case-insensitive.
*
* Routing:
* - .mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2 → read_mps()
* - .lp, .lp.gz, .lp.bz2 → read_lp()
* Routing (case-insensitive extensions):
* - .mps, .mps.gz, .mps.bz2, .mps.lz4, .qps, .qps.gz, .qps.bz2, .qps.lz4
* → read_mps() when mps_reader == default_reader, or read_mps_fast_experimental()
* when mps_reader == fast_experimental (fixed_mps_format must be false)
* - .lp, .lp.gz, .lp.bz2, .lp.lz4 → read_lp()
* - anything else → std::logic_error
*
* This is the entry point of choice for user-facing tools (CLI, C API) that
* want both formats to "just work" without an explicit format flag.
*
* @param[in] path Path to the input file.
* @param[in] mps_reader Selects the MPS reader implementation for MPS/QPS inputs.
* @param[in] fixed_mps_format If the MPS/QPS reader should use fixed format;
* ignored for LP inputs. False by default.
* @return mps_data_model_t The parsed problem.
*/
template <typename i_t, typename f_t>
inline mps_data_model_t<i_t, f_t> read(const std::string& path, bool fixed_mps_format = false)
inline mps_data_model_t<i_t, f_t> read(const std::string& path,
mps_reader_type_t mps_reader,
bool fixed_mps_format = false)
{
std::string lower(path);
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
if (lower.ends_with(".mps") || lower.ends_with(".mps.gz") || lower.ends_with(".mps.bz2") ||
lower.ends_with(".qps") || lower.ends_with(".qps.gz") || lower.ends_with(".qps.bz2")) {
return read_mps<i_t, f_t>(path, fixed_mps_format);
for (const char* compression_suffix : {".bz2", ".gz", ".lz4"}) {
if (lower.ends_with(compression_suffix)) {
lower.resize(lower.size() - std::strlen(compression_suffix));
break;
}
}
if (lower.ends_with(".lp") || lower.ends_with(".lp.gz") || lower.ends_with(".lp.bz2")) {
return read_lp<i_t, f_t>(path);
if (lower.ends_with(".mps") || lower.ends_with(".qps")) {
if (mps_reader == mps_reader_type_t::fast_experimental) {
if (fixed_mps_format) {
throw std::logic_error(
"experimental fast MPS reader does not support fixed MPS format forcing");
}
return read_mps_fast_experimental<i_t, f_t>(path);
}
return read_mps<i_t, f_t>(path, fixed_mps_format);
}
if (lower.ends_with(".lp")) { return read_lp<i_t, f_t>(path); }
throw std::logic_error(
"read: unrecognized input file extension. Supported (case-insensitive): "
".mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2, .lp, .lp.gz, .lp.bz2. "
".mps, .mps.gz, .mps.bz2, .mps.lz4, .qps, .qps.gz, .qps.bz2, .qps.lz4, "
".lp, .lp.gz, .lp.bz2, .lp.lz4. "
"Given path: " +
path);
}

/**
* @brief Reads an optimization problem from a file, dispatching on the file
* extension. Extension matching is case-insensitive.
*
* Uses the default MPS reader. See the 3-argument read() overload for routing
* details and supported extensions.
*
* @param[in] path Path to the input file.
* @param[in] fixed_mps_format If the MPS/QPS reader should use fixed format;
* ignored for LP inputs. False by default.
* @return mps_data_model_t The parsed problem.
*/
template <typename i_t, typename f_t>
inline mps_data_model_t<i_t, f_t> read(const std::string& path, bool fixed_mps_format = false)
{
return read<i_t, f_t>(path, mps_reader_type_t::default_reader, fixed_mps_format);
}

} // namespace cuopt::linear_programming::io
1 change: 1 addition & 0 deletions cpp/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ add_subdirectory(branch_and_bound)
add_subdirectory(cuts)

set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${UTIL_SRC_FILES} PARENT_SCOPE)
set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE)
Loading
Loading