Skip to content

Commit 3e8acd7

Browse files
authored
Fast MPS parser for free-format MPS files (#1429)
## Issue Authors: - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Nicolas L. Guidotti (https://github.com/nguidotti) - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) URL: #1429
1 parent fe2a8f7 commit 3e8acd7

31 files changed

Lines changed: 8756 additions & 73 deletions

ci/build_wheel_libcuopt.sh

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,34 @@ fi
1717
# Install Boost and TBB
1818
bash ci/utils/install_boost_tbb.sh
1919

20-
# Install libuuid (needed by cuopt_grpc_server)
20+
# Install libuuid and LLVM's OpenMP runtime
2121
if command -v dnf &> /dev/null; then
22-
dnf install -y libuuid-devel
22+
# LLVM Toolset is distributed as a module on Rocky/RHEL 8.
23+
dnf module install -y llvm-toolset
24+
dnf install -y libuuid-devel libomp-devel
2325
elif command -v apt-get &> /dev/null; then
24-
apt-get update && apt-get install -y uuid-dev
26+
apt-get update
27+
apt-get install -y uuid-dev libomp-dev
2528
fi
2629

2730
# Install Protobuf + gRPC (protoc + grpc_cpp_plugin)
2831
bash ci/utils/install_protobuf_grpc.sh
2932

30-
export SKBUILD_CMAKE_ARGS="-DCUOPT_BUILD_WHEELS=ON;-DDISABLE_DEPRECATION_WARNING=ON"
33+
# Compile with GCC, but use LLVM libomp as the OpenMP runtime bundled in the wheel. Resolve the
34+
# versioned ELF library rather than an unversioned linker script or compiler-toolset indirection.
35+
LIBOMP_LIBRARY="$(
36+
ldconfig -p |
37+
awk '$1 ~ /^libomp\.so(\.[0-9]+)*$/ && !library { library = $NF }
38+
END { print library }'
39+
)"
40+
if [[ "${LIBOMP_LIBRARY}" != /* || ! -f "${LIBOMP_LIBRARY}" ]]; then
41+
echo "Could not resolve the LLVM OpenMP runtime: '${LIBOMP_LIBRARY}'" >&2
42+
exit 1
43+
fi
44+
45+
echo "Using LLVM OpenMP runtime: ${LIBOMP_LIBRARY}"
46+
47+
export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${LIBOMP_LIBRARY}"
3148

3249
# OpenSSL 3 hints for libcuopt's own find_package(OpenSSL).
3350
#

cpp/CMakeLists.txt

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ list(APPEND CUOPT_CUDA_FLAGS -Xfatbin=-compress-all)
186186
if (CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9 AND CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 13.0)
187187
list(APPEND CUOPT_CUDA_FLAGS -Xfatbin=--compress-level=3)
188188
endif ()
189-
list(APPEND CUOPT_CUDA_FLAGS -fopenmp)
190189

191190
# Add jobserver flags for parallel compilation if PARALLEL_LEVEL is set
192191
if (PARALLEL_LEVEL AND NOT "${PARALLEL_LEVEL}" STREQUAL "")
@@ -198,13 +197,14 @@ if (PARALLEL_LEVEL AND NOT "${PARALLEL_LEVEL}" STREQUAL "")
198197
endif ()
199198
endif ()
200199

201-
# The MIP solver requires OpenMP to work
202-
find_package(OpenMP REQUIRED)
200+
# The MIP solver requires OpenMP for both C++ and CUDA host code.
201+
find_package(OpenMP REQUIRED COMPONENTS CXX CUDA)
203202
message(VERBOSE "cuOpt: OpenMP found in ${OpenMP_CXX_INCLUDE_DIRS}")
204203

205-
# MPS/QPS parser supports compressed inputs via bzip2 and zlib
204+
# MPS/QPS parser supports compressed inputs via bzip2, zlib and lz4
206205
option(CUOPT_PARSER_WITH_BZIP2 "Build MPS parser with bzip2 decompression" ON)
207206
option(CUOPT_PARSER_WITH_ZLIB "Build MPS parser with zlib decompression" ON)
207+
option(CUOPT_PARSER_WITH_LZ4 "Build experimental fast MPS parser with LZ4 decompression" ON)
208208
if (CUOPT_PARSER_WITH_BZIP2)
209209
find_package(BZip2 REQUIRED)
210210
add_compile_definitions(MPS_PARSER_WITH_BZIP2)
@@ -213,6 +213,10 @@ if (CUOPT_PARSER_WITH_ZLIB)
213213
find_package(ZLIB REQUIRED)
214214
add_compile_definitions(MPS_PARSER_WITH_ZLIB)
215215
endif ()
216+
if (CUOPT_PARSER_WITH_LZ4)
217+
# No headers or link target needed; the experimental reader loads one liblz4 symbol at runtime.
218+
add_compile_definitions(MPS_PARSER_WITH_LZ4)
219+
endif ()
216220

217221
# Debug options
218222
if (CMAKE_BUILD_TYPE MATCHES Debug)
@@ -250,6 +254,20 @@ else ()
250254
find_package(RAFT REQUIRED)
251255
endif ()
252256

257+
rapids_cpm_find(simde 0.8.2
258+
CPM_ARGS
259+
GIT_REPOSITORY https://github.com/simd-everywhere/simde.git
260+
GIT_TAG v0.8.2
261+
GIT_SHALLOW TRUE
262+
DOWNLOAD_ONLY TRUE
263+
)
264+
265+
if (NOT TARGET simde::simde)
266+
add_library(simde::simde INTERFACE IMPORTED GLOBAL)
267+
set_target_properties(simde::simde
268+
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${simde_SOURCE_DIR}")
269+
endif ()
270+
253271
FetchContent_Declare(
254272
papilo
255273
GIT_REPOSITORY "https://github.com/nguidotti/papilo.git"
@@ -436,6 +454,7 @@ if (BUILD_TESTS)
436454
endif ()
437455

438456
set(CUOPT_SRC_FILES)
457+
set(MPS_FAST_SRC_FILES)
439458
add_subdirectory(src)
440459

441460
# nvcc 13.0.3 ICE (signal 11) compiling sliding_window.cu with 7 GPU architectures;
@@ -445,14 +464,24 @@ set_source_files_properties(
445464
PROPERTIES COMPILE_OPTIONS "--split-compile=0")
446465

447466
if (HOST_LINEINFO)
448-
set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1")
467+
set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1")
468+
endif ()
469+
470+
# Needed for the fast MPS parser, available on all x86-64-v3 compliant x86 CPUs (essentially since Haswell ~2013)
471+
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND
472+
CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
473+
set_property(SOURCE ${MPS_FAST_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
474+
APPEND PROPERTY COMPILE_OPTIONS "-mbmi2;-mavx2;-msse4.2")
449475
endif ()
450476

477+
# TODO: figure out a set of flags for ARM that fits the range of CPUs we wish to support (neoverse?)
478+
# NEON should be universal on aarch64 and enough for our purposes (parsing) though
479+
451480
# Apply -UNDEBUG only to solver source files (not gRPC infrastructure).
452481
# Must happen before gRPC files are appended to CUOPT_SRC_FILES.
453482
# Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO).
454483
if (DEFINE_ASSERT)
455-
set_property(SOURCE ${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_SOURCE_DIR}
484+
set_property(SOURCE ${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
456485
APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG")
457486
endif ()
458487

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

@@ -605,11 +634,13 @@ target_link_libraries(cuopt
605634
${CUDSS_LIB_FILE}
606635
PRIVATE
607636
${CUOPT_PRIVATE_CUDA_LIBS}
637+
simde::simde
638+
OpenMP::OpenMP_CXX
639+
OpenMP::OpenMP_CUDA
608640
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:protobuf::libprotobuf>
609641
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:gRPC::grpc++>
610642
)
611643

612-
613644
# ##################################################################################################
614645
# - generate tests --------------------------------------------------------------------------------
615646
if (BUILD_TESTS)

cpp/cuopt_cli.cpp

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ inline cuopt::init_logger_t dummy_logger(
9090
* .mps/.qps and their .gz/.bz2 variants → MPS parser;
9191
* anything else is rejected.
9292
* @param initial_solution_file Path to initial solution file in SOL format
93+
* @param mps_reader MPS reader implementation selected by the CLI
9394
* @param settings Merged solver settings (config file loaded in main, then CLI overrides applied)
9495
*/
9596
int run_single_file(const std::string& file_path,
9697
const std::string& initial_solution_file,
9798
bool solve_relaxation,
99+
cuopt::mathematical_optimization::io::mps_reader_type_t mps_reader,
98100
cuopt::mathematical_optimization::solver_settings_t<int, double>& settings)
99101
{
100102
cuopt::init_logger_t log(settings.get_parameter<std::string>(CUOPT_LOG_FILE),
@@ -108,7 +110,8 @@ int run_single_file(const std::string& file_path,
108110
{
109111
CUOPT_LOG_INFO("Reading file %s", base_filename.c_str());
110112
try {
111-
mps_data_model = cuopt::mathematical_optimization::io::read<int, double>(file_path);
113+
mps_data_model =
114+
cuopt::mathematical_optimization::io::read<int, double>(file_path, mps_reader);
112115
} catch (const std::logic_error& e) {
113116
CUOPT_LOG_ERROR("Parser exception: %s", e.what());
114117
parsing_failed = true;
@@ -287,8 +290,8 @@ int main(int argc, char* argv[])
287290
program.add_argument("filename")
288291
.help(
289292
"input problem file; format dispatched by extension (case-insensitive). "
290-
"Supported: .lp, .mps, .qps and their .gz / .bz2 compressed variants "
291-
"(e.g. .lp.gz, .mps.bz2, .qps.gz)")
293+
"Supported: .lp, .mps, .qps and their .gz / .bz2 / .lz4 compressed variants "
294+
"(e.g. .lp.gz, .mps.bz2, .qps.lz4).")
292295
.nargs(1)
293296
.required();
294297

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

312+
program.add_argument("--mps-reader")
313+
.help(
314+
"MPS reader implementation: default uses the production parser; experimental-fast uses the "
315+
"experimental SIMD parser for free-format LP/MIP/QP/QCQP (SOCP) .mps/.qps files and their "
316+
".gz/.bz2/.lz4 compressed variants")
317+
.default_value(std::string("default"))
318+
.choices("default", "experimental-fast");
319+
309320
program.add_argument("--dump-hyper-params")
310321
.help("print hyper-parameters only in config file format and exit")
311322
.default_value(false)
@@ -406,6 +417,12 @@ int main(int argc, char* argv[])
406417
const auto initial_solution_file = program.get<std::string>("--initial-solution");
407418
const auto solve_relaxation = program.get<bool>("--relaxation");
408419
const auto params_file = program.get<std::string>("--params-file");
420+
const auto mps_reader_arg = program.get<std::string>("--mps-reader");
421+
422+
auto mps_reader = cuopt::mathematical_optimization::io::mps_reader_type_t::default_reader;
423+
if (mps_reader_arg == "experimental-fast") {
424+
mps_reader = cuopt::mathematical_optimization::io::mps_reader_type_t::fast_experimental;
425+
}
409426

410427
cuopt::mathematical_optimization::solver_settings_t<int, double> settings;
411428
try {
@@ -435,5 +452,5 @@ int main(int argc, char* argv[])
435452
RAFT_CUDA_TRY(cudaSetDevice(0));
436453
}
437454

438-
return run_single_file(file_name, initial_solution_file, solve_relaxation, settings);
455+
return run_single_file(file_name, initial_solution_file, solve_relaxation, mps_reader, settings);
439456
}

cpp/include/cuopt/mathematical_optimization/io/parser.hpp

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,26 @@
1111

1212
#include <algorithm>
1313
#include <cctype>
14+
#include <cstring>
1415
#include <stdexcept>
1516
#include <string>
1617
#include <string_view>
1718

1819
namespace cuopt::mathematical_optimization::io {
1920

21+
/**
22+
* @brief Selects which MPS reader implementation should be used by dispatching entry points.
23+
*
24+
* The experimental fast reader is intentionally opt-in. It supports the same free-format
25+
* MPS/QPS scope as read_mps(): LP, MIP, QP (QUADOBJ/QMATRIX), and QCQP/SOCP (QCMATRIX).
26+
*/
27+
enum class mps_reader_type_t { default_reader, fast_experimental };
28+
2029
/**
2130
* @brief Reads the equation from an MPS or QPS file.
2231
*
2332
* The input file can be a plain text file in MPS-/QPS-format or a compressed MPS/QPS
24-
* file (.mps.gz or .mps.bz2).
33+
* file (.mps.gz, .mps.bz2, or .mps.lz4).
2534
*
2635
* Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more
2736
* details on both free and fixed MPS format.
@@ -32,8 +41,8 @@ namespace cuopt::mathematical_optimization::io {
3241
* - QMATRIX: Full symmetric quadratic objective matrix (alternative to QUADOBJ)
3342
* - QCMATRIX: Symmetric quadratic terms for a named constraint row (QCQP)
3443
*
35-
* Note: Compressed MPS files .mps.gz, .mps.bz2 can only be read if the compression
36-
* libraries zlib or libbzip2 are installed, respectively.
44+
* Note: Compressed MPS files .mps.gz, .mps.bz2, and .mps.lz4 can only be read if
45+
* zlib, libbzip2, or liblz4 are installed, respectively.
3746
*
3847
* @param[in] mps_file_path Path to MPS/QPSfile.
3948
* @param[in] fixed_mps_format If MPS/QPS file should be parsed as fixed, false by default
@@ -43,6 +52,19 @@ template <typename i_t, typename f_t>
4352
mps_data_model_t<i_t, f_t> read_mps(const std::string& mps_file_path,
4453
bool fixed_mps_format = false);
4554

55+
/**
56+
* @brief Reads an MPS/QPS problem with the experimental SIMD-optimized reader.
57+
*
58+
* Supports the same free-format LP/MIP/QP/QCQP (SOCP-relevant QCMATRIX) scope as read_mps().
59+
* Fixed MPS format forcing is not supported. Accepts .mps/.qps and their .gz/.bz2/.lz4 variants
60+
* (compression is detected from the file path, same as read_mps()).
61+
*
62+
* @param[in] mps_file_path Path to a raw or compressed .mps or .qps file.
63+
* @return mps_data_model_t A fully formed LP/MIP/QP problem which represents the given file.
64+
*/
65+
template <typename i_t, typename f_t>
66+
mps_data_model_t<i_t, f_t> read_mps_fast_experimental(const std::string& mps_file_path);
67+
4668
/**
4769
* @brief Reads an MPS problem from in-memory file contents.
4870
*
@@ -111,38 +133,72 @@ mps_data_model_t<i_t, f_t> read_lp_from_string(std::string_view lp_contents);
111133
* @brief Reads an optimization problem from a file, dispatching on the file
112134
* extension. Extension matching is case-insensitive.
113135
*
114-
* Routing:
115-
* - .mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2 → read_mps()
116-
* - .lp, .lp.gz, .lp.bz2 → read_lp()
136+
* Routing (case-insensitive extensions):
137+
* - .mps, .mps.gz, .mps.bz2, .mps.lz4, .qps, .qps.gz, .qps.bz2, .qps.lz4
138+
* → read_mps() when mps_reader == default_reader, or read_mps_fast_experimental()
139+
* when mps_reader == fast_experimental (fixed_mps_format must be false)
140+
* - .lp, .lp.gz, .lp.bz2, .lp.lz4 → read_lp()
117141
* - anything else → std::logic_error
118142
*
119143
* This is the entry point of choice for user-facing tools (CLI, C API) that
120144
* want both formats to "just work" without an explicit format flag.
121145
*
122146
* @param[in] path Path to the input file.
147+
* @param[in] mps_reader Selects the MPS reader implementation for MPS/QPS inputs.
123148
* @param[in] fixed_mps_format If the MPS/QPS reader should use fixed format;
124149
* ignored for LP inputs. False by default.
125150
* @return mps_data_model_t The parsed problem.
126151
*/
127152
template <typename i_t, typename f_t>
128-
inline mps_data_model_t<i_t, f_t> read(const std::string& path, bool fixed_mps_format = false)
153+
inline mps_data_model_t<i_t, f_t> read(const std::string& path,
154+
mps_reader_type_t mps_reader,
155+
bool fixed_mps_format = false)
129156
{
130157
std::string lower(path);
131158
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) {
132159
return static_cast<char>(std::tolower(c));
133160
});
134-
if (lower.ends_with(".mps") || lower.ends_with(".mps.gz") || lower.ends_with(".mps.bz2") ||
135-
lower.ends_with(".qps") || lower.ends_with(".qps.gz") || lower.ends_with(".qps.bz2")) {
136-
return read_mps<i_t, f_t>(path, fixed_mps_format);
161+
for (const char* compression_suffix : {".bz2", ".gz", ".lz4"}) {
162+
if (lower.ends_with(compression_suffix)) {
163+
lower.resize(lower.size() - std::strlen(compression_suffix));
164+
break;
165+
}
137166
}
138-
if (lower.ends_with(".lp") || lower.ends_with(".lp.gz") || lower.ends_with(".lp.bz2")) {
139-
return read_lp<i_t, f_t>(path);
167+
if (lower.ends_with(".mps") || lower.ends_with(".qps")) {
168+
if (mps_reader == mps_reader_type_t::fast_experimental) {
169+
if (fixed_mps_format) {
170+
throw std::logic_error(
171+
"experimental fast MPS reader does not support fixed MPS format forcing");
172+
}
173+
return read_mps_fast_experimental<i_t, f_t>(path);
174+
}
175+
return read_mps<i_t, f_t>(path, fixed_mps_format);
140176
}
177+
if (lower.ends_with(".lp")) { return read_lp<i_t, f_t>(path); }
141178
throw std::logic_error(
142179
"read: unrecognized input file extension. Supported (case-insensitive): "
143-
".mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2, .lp, .lp.gz, .lp.bz2. "
180+
".mps, .mps.gz, .mps.bz2, .mps.lz4, .qps, .qps.gz, .qps.bz2, .qps.lz4, "
181+
".lp, .lp.gz, .lp.bz2, .lp.lz4. "
144182
"Given path: " +
145183
path);
146184
}
147185

186+
/**
187+
* @brief Reads an optimization problem from a file, dispatching on the file
188+
* extension. Extension matching is case-insensitive.
189+
*
190+
* Uses the default MPS reader. See the 3-argument read() overload for routing
191+
* details and supported extensions.
192+
*
193+
* @param[in] path Path to the input file.
194+
* @param[in] fixed_mps_format If the MPS/QPS reader should use fixed format;
195+
* ignored for LP inputs. False by default.
196+
* @return mps_data_model_t The parsed problem.
197+
*/
198+
template <typename i_t, typename f_t>
199+
inline mps_data_model_t<i_t, f_t> read(const std::string& path, bool fixed_mps_format = false)
200+
{
201+
return read<i_t, f_t>(path, mps_reader_type_t::default_reader, fixed_mps_format);
202+
}
203+
148204
} // namespace cuopt::mathematical_optimization::io

cpp/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ add_subdirectory(branch_and_bound)
2626
add_subdirectory(cuts)
2727

2828
set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${UTIL_SRC_FILES} PARENT_SCOPE)
29+
set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE)

cpp/src/io/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
# SPDX-License-Identifier: Apache-2.0
44
# cmake-format: on
55

6+
set(MPS_FAST_SRC_FILES
7+
${CMAKE_CURRENT_SOURCE_DIR}/experimental_mps_fast/fast_parser.cpp
8+
${CMAKE_CURRENT_SOURCE_DIR}/experimental_mps_fast/file_reader.cpp
9+
${CMAKE_CURRENT_SOURCE_DIR}/experimental_mps_fast/lz4_file_reader.cpp
10+
${CMAKE_CURRENT_SOURCE_DIR}/experimental_mps_fast/mps_section_scanner.cpp
11+
)
12+
613
set(PARSERS_SRC_FILES
714
${CMAKE_CURRENT_SOURCE_DIR}/data_model_view.cpp
815
${CMAKE_CURRENT_SOURCE_DIR}/file_to_string.cpp
@@ -13,6 +20,8 @@ set(PARSERS_SRC_FILES
1320
${CMAKE_CURRENT_SOURCE_DIR}/parser.cpp
1421
${CMAKE_CURRENT_SOURCE_DIR}/writer.cpp
1522
${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython_parser.cpp
23+
${MPS_FAST_SRC_FILES}
1624
)
1725

1826
set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE)
27+
set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE)

0 commit comments

Comments
 (0)