Skip to content

Commit 536488d

Browse files
authored
Decompression for .mps.gz and .mps.bz2 files (#357)
Adding zlib and bzip2 decompression to mps_parser, such that .mps.gz and .mps.bz2 files can be opened directly. Authors: - https://github.com/ahehn-nv - Ramakrishnap (https://github.com/rgsl888prabhu) Approvers: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Nicolas Blin (https://github.com/Kh4ster) - Alice Boucher (https://github.com/aliceb-nv) - Ishika Roy (https://github.com/Iroy30) URL: #357
1 parent bc033e4 commit 536488d

File tree

15 files changed

+433
-10
lines changed

15 files changed

+433
-10
lines changed

ci/build_wheel_cuopt_mps_parser.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ fi
3333

3434
ci/build_wheel.sh cuopt_mps_parser ${package_dir}
3535

36+
37+
EXCLUDE_ARGS=(
38+
--exclude "libzlib.so"
39+
--exclude "libbz2.so"
40+
)
41+
3642
# repair wheels and write to the location that artifact-uploading code expects to find them
37-
python -m auditwheel repair -w "${RAPIDS_WHEEL_BLD_OUTPUT_DIR}" ${package_dir}/dist/*
43+
python -m auditwheel repair "${EXCLUDE_ARGS[@]}" -w "${RAPIDS_WHEEL_BLD_OUTPUT_DIR}" ${package_dir}/dist/*
3844

3945
ci/validate_wheel.sh "${package_dir}" "${RAPIDS_WHEEL_BLD_OUTPUT_DIR}"

conda/environments/all_cuda-129_arch-aarch64.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ channels:
77
dependencies:
88
- boost
99
- breathe
10+
- bzip2
1011
- c-compiler
1112
- ccache
1213
- clang-tools=20.1.4
@@ -79,6 +80,7 @@ dependencies:
7980
- sphinxcontrib-websupport
8081
- sysroot_linux-aarch64==2.28
8182
- uvicorn==0.34.*
83+
- zlib
8284
- pip:
8385
- nvidia_sphinx_theme
8486
- swagger-plugin-for-sphinx

conda/environments/all_cuda-129_arch-x86_64.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ channels:
77
dependencies:
88
- boost
99
- breathe
10+
- bzip2
1011
- c-compiler
1112
- ccache
1213
- clang-tools=20.1.4
@@ -79,6 +80,7 @@ dependencies:
7980
- sphinxcontrib-websupport
8081
- sysroot_linux-64==2.28
8182
- uvicorn==0.34.*
83+
- zlib
8284
- pip:
8385
- nvidia_sphinx_theme
8486
- swagger-plugin-for-sphinx

conda/recipes/libcuopt/recipe.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ cache:
5757
- cuda-version =${{ cuda_version }}
5858
- cmake ${{ cmake_version }}
5959
- ninja
60+
- zlib
61+
- bzip2
6062
host:
6163
- cpp-argparse
6264
- cuda-version =${{ cuda_version }}
@@ -70,6 +72,8 @@ cache:
7072
- libcusparse-dev
7173
- cuda-cudart-dev
7274
- boost
75+
- zlib
76+
- bzip2
7377

7478
outputs:
7579
- package:
@@ -90,6 +94,14 @@ outputs:
9094
build:
9195
- cmake ${{ cmake_version }}
9296
- ${{ stdlib("c") }}
97+
- zlib
98+
- bzip2
99+
host:
100+
- zlib
101+
- bzip2
102+
run:
103+
- zlib
104+
- bzip2
93105
ignore_run_exports:
94106
by_name:
95107
- cuda-cudart
@@ -99,6 +111,8 @@ outputs:
99111
- libcurand
100112
- libcusparse
101113
- librmm
114+
- libzlib
115+
- libbz2
102116
tests:
103117
- package_contents:
104118
files:

conda/recipes/mps-parser/recipe.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ requirements:
3232
build:
3333
- cmake ${{ cmake_version }}
3434
- ninja
35+
- libmps-parser =${{ version }}
3536
- ${{ compiler("c") }}
3637
- ${{ compiler("cxx") }}
3738
- ${{ stdlib("c") }}

cpp/libmps_parser/CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ rapids_cmake_build_type(Release)
3838
# #############################################################################
3939
# - User Options ------------------------------------------------------------
4040
option(BUILD_TESTS "Configure CMake to build tests" ON)
41+
option(MPS_PARSER_WITH_BZIP2 "Build with bzip2 decompression" ON)
42+
option(MPS_PARSER_WITH_ZLIB "Build with zlib decompression" ON)
4143

4244
message(VERBOSE "cuOpt: Build mps-parser unit-tests: ${BUILD_TESTS}")
4345

@@ -50,6 +52,16 @@ if(CMAKE_COMPILER_IS_GNUCXX)
5052
list(APPEND MPS_PARSER_CXX_FLAGS -Werror -Wno-error=deprecated-declarations)
5153
endif(CMAKE_COMPILER_IS_GNUCXX)
5254

55+
if(MPS_PARSER_WITH_BZIP2)
56+
find_package(BZip2 REQUIRED)
57+
add_compile_definitions(-DMPS_PARSER_WITH_BZIP2)
58+
endif(MPS_PARSER_WITH_BZIP2)
59+
60+
if(MPS_PARSER_WITH_ZLIB)
61+
find_package(ZLIB REQUIRED)
62+
add_compile_definitions(-DMPS_PARSER_WITH_ZLIB)
63+
endif(MPS_PARSER_WITH_ZLIB)
64+
5365
if(DEFINE_ASSERT)
5466
add_definitions(-DASSERT_MODE)
5567
endif(DEFINE_ASSERT)
@@ -109,6 +121,14 @@ target_include_directories(mps_parser
109121
"$<INSTALL_INTERFACE:include>"
110122
)
111123

124+
if(MPS_PARSER_WITH_BZIP2)
125+
target_include_directories(mps_parser PRIVATE BZip2::BZip2)
126+
endif(MPS_PARSER_WITH_BZIP2)
127+
128+
if(MPS_PARSER_WITH_ZLIB)
129+
target_include_directories(mps_parser PRIVATE ZLIB::ZLIB)
130+
endif(MPS_PARSER_WITH_ZLIB)
131+
112132
# ##################################################################################################
113133
# - generate tests --------------------------------------------------------------------------------
114134
if(BUILD_TESTS)

cpp/libmps_parser/include/mps_parser/parser.hpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,24 @@
2222
namespace cuopt::mps_parser {
2323

2424
/**
25-
* @brief Reads the equation from the input text file which is MPS or QPS formatted
25+
* @brief Reads the equation from an MPS or QPS file.
26+
*
27+
* The input file can be a plain text file in MPS-/QPS-format or a compressed MPS/QPS
28+
* file (.mps.gz or .mps.bz2).
2629
*
2730
* Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more
2831
* details on both free and fixed MPS format.
29-
*
3032
* This function supports both standard MPS files (for linear programming) and
3133
* QPS files (for quadratic programming). QPS files are MPS files with additional
3234
* sections:
3335
* - QUADOBJ: Defines quadratic terms in the objective function
3436
*
35-
* @param[in] mps_file_path Path to MPS or QPS formatted file.
36-
* @param[in] fixed_mps_format If MPS/QPS file should be parsed as fixed format, false by default
37-
* @return mps_data_model_t A fully formed LP/QP problem which represents the given MPS/QPS file
37+
* Note: Compressed MPS files .mps.gz, .mps.bz2 can only be read if the compression
38+
* libraries zlib or libbzip2 are installed, respectively.
39+
*
40+
* @param[in] mps_file_path Path to MPS/QPSfile.
41+
* @param[in] fixed_mps_format If MPS/QPS file should be parsed as fixed, false by default
42+
* @return mps_data_model_t A fully formed LP/QP problem which represents the given file
3843
*/
3944
template <typename i_t, typename f_t>
4045
mps_data_model_t<i_t, f_t> parse_mps(const std::string& mps_file_path,

cpp/libmps_parser/src/mps_parser.cpp

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,193 @@
2626
#include <fstream>
2727
#include <iostream>
2828
#include <limits>
29+
#include <memory>
2930
#include <sstream>
3031
#include <string>
3132

33+
#ifdef MPS_PARSER_WITH_BZIP2
34+
#include <bzlib.h>
35+
#endif // MPS_PARSER_WITH_BZIP2
36+
37+
#ifdef MPS_PARSER_WITH_ZLIB
38+
#include <zlib.h>
39+
#endif // MPS_PARSER_WITH_ZLIB
40+
41+
#if defined(MPS_PARSER_WITH_BZIP2) || defined(MPS_PARSER_WITH_ZLIB)
42+
#include <dlfcn.h>
43+
#endif // MPS_PARSER_WITH_BZIP2 || MPS_PARSER_WITH_ZLIB
44+
45+
namespace {
46+
using cuopt::mps_parser::error_type_t;
47+
using cuopt::mps_parser::mps_parser_expects;
48+
using cuopt::mps_parser::mps_parser_expects_fatal;
49+
50+
struct FcloseDeleter {
51+
void operator()(FILE* fp)
52+
{
53+
mps_parser_expects_fatal(
54+
fclose(fp) == 0, error_type_t::ValidationError, "Error closing MPS file!");
55+
}
56+
};
57+
} // end namespace
58+
59+
#ifdef MPS_PARSER_WITH_BZIP2
60+
namespace {
61+
using BZ2_bzReadOpen_t = decltype(&BZ2_bzReadOpen);
62+
using BZ2_bzReadClose_t = decltype(&BZ2_bzReadClose);
63+
using BZ2_bzRead_t = decltype(&BZ2_bzRead);
64+
65+
std::vector<char> bz2_file_to_string(const std::string& file)
66+
{
67+
struct DlCloseDeleter {
68+
void operator()(void* fp)
69+
{
70+
mps_parser_expects_fatal(
71+
dlclose(fp) == 0, error_type_t::ValidationError, "Error closing libbz2.so!");
72+
}
73+
};
74+
struct BzReadCloseDeleter {
75+
void operator()(void* f)
76+
{
77+
int bzerror;
78+
if (f != nullptr) fptr(&bzerror, f);
79+
mps_parser_expects_fatal(
80+
bzerror == BZ_OK, error_type_t::ValidationError, "Error closing bzip2 file!");
81+
}
82+
BZ2_bzReadClose_t fptr = nullptr;
83+
};
84+
85+
std::unique_ptr<void, DlCloseDeleter> lbz2handle{dlopen("libbz2.so", RTLD_LAZY)};
86+
mps_parser_expects(
87+
lbz2handle != nullptr,
88+
error_type_t::ValidationError,
89+
"Could not open .mps.bz2 file since libbz2.so was not found. In order to open .mps.bz2 files "
90+
"directly, please ensure libbzip2 is installed. Alternatively, decompress the .mps.bz2 file "
91+
"manually and open the uncompressed .mps file. Given path: %s",
92+
file.c_str());
93+
94+
BZ2_bzReadOpen_t BZ2_bzReadOpen =
95+
reinterpret_cast<BZ2_bzReadOpen_t>(dlsym(lbz2handle.get(), "BZ2_bzReadOpen"));
96+
BZ2_bzReadClose_t BZ2_bzReadClose =
97+
reinterpret_cast<BZ2_bzReadClose_t>(dlsym(lbz2handle.get(), "BZ2_bzReadClose"));
98+
BZ2_bzRead_t BZ2_bzRead = reinterpret_cast<BZ2_bzRead_t>(dlsym(lbz2handle.get(), "BZ2_bzRead"));
99+
mps_parser_expects(
100+
BZ2_bzReadOpen != nullptr && BZ2_bzReadClose != nullptr && BZ2_bzRead != nullptr,
101+
error_type_t::ValidationError,
102+
"Error loading libbzip2! Library version might be incompatible. Please decompress the .mps.bz2 "
103+
"file manually and open the uncompressed .mps file. Given path: %s",
104+
file.c_str());
105+
106+
std::unique_ptr<FILE, FcloseDeleter> fp{fopen(file.c_str(), "rb")};
107+
mps_parser_expects(fp != nullptr,
108+
error_type_t::ValidationError,
109+
"Error opening MPS file! Given path: %s",
110+
file.c_str());
111+
int bzerror = BZ_OK;
112+
std::unique_ptr<void, BzReadCloseDeleter> bzfile{
113+
BZ2_bzReadOpen(&bzerror, fp.get(), 0, 0, nullptr, 0), {BZ2_bzReadClose}};
114+
mps_parser_expects(bzerror == BZ_OK,
115+
error_type_t::ValidationError,
116+
"Could not open bzip2 compressed file! Given path: %s",
117+
file.c_str());
118+
119+
std::vector<char> buf;
120+
const size_t readbufsize = 1ull << 24; // 16MiB - just a guess.
121+
std::vector<char> readbuf(readbufsize);
122+
while (bzerror == BZ_OK) {
123+
const size_t bytes_read = BZ2_bzRead(&bzerror, bzfile.get(), readbuf.data(), readbuf.size());
124+
if (bzerror == BZ_OK || bzerror == BZ_STREAM_END) {
125+
buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read);
126+
}
127+
}
128+
buf.push_back('\0');
129+
mps_parser_expects(bzerror == BZ_STREAM_END,
130+
error_type_t::ValidationError,
131+
"Error in bzip2 decompression of MPS file! Given path: %s",
132+
file.c_str());
133+
return buf;
134+
}
135+
} // end namespace
136+
#endif // MPS_PARSER_WITH_BZIP2
137+
138+
#ifdef MPS_PARSER_WITH_ZLIB
139+
namespace {
140+
using gzopen_t = decltype(&gzopen);
141+
using gzclose_r_t = decltype(&gzclose_r);
142+
using gzbuffer_t = decltype(&gzbuffer);
143+
using gzread_t = decltype(&gzread);
144+
using gzerror_t = decltype(&gzerror);
145+
std::vector<char> zlib_file_to_string(const std::string& file)
146+
{
147+
struct DlCloseDeleter {
148+
void operator()(void* fp)
149+
{
150+
mps_parser_expects_fatal(
151+
dlclose(fp) == 0, error_type_t::ValidationError, "Error closing libbz2.so!");
152+
}
153+
};
154+
struct GzCloseDeleter {
155+
void operator()(gzFile_s* f)
156+
{
157+
int err = fptr(f);
158+
mps_parser_expects_fatal(
159+
err == Z_OK, error_type_t::ValidationError, "Error closing gz file!");
160+
}
161+
gzclose_r_t fptr = nullptr;
162+
};
163+
164+
std::unique_ptr<void, DlCloseDeleter> lzhandle{dlopen("libz.so.1", RTLD_LAZY)};
165+
mps_parser_expects(
166+
lzhandle != nullptr,
167+
error_type_t::ValidationError,
168+
"Could not open .mps.gz file since libz.so was not found. In order to open .mps.gz files "
169+
"directly, please ensure zlib is installed. Alternatively, decompress the .mps.gz file "
170+
"manually and open the uncompressed .mps file. Given path: %s",
171+
file.c_str());
172+
gzopen_t gzopen = reinterpret_cast<gzopen_t>(dlsym(lzhandle.get(), "gzopen"));
173+
gzclose_r_t gzclose_r = reinterpret_cast<gzclose_r_t>(dlsym(lzhandle.get(), "gzclose_r"));
174+
gzbuffer_t gzbuffer = reinterpret_cast<gzbuffer_t>(dlsym(lzhandle.get(), "gzbuffer"));
175+
gzread_t gzread = reinterpret_cast<gzread_t>(dlsym(lzhandle.get(), "gzread"));
176+
gzerror_t gzerror = reinterpret_cast<gzerror_t>(dlsym(lzhandle.get(), "gzerror"));
177+
mps_parser_expects(
178+
gzopen != nullptr && gzclose_r != nullptr && gzbuffer != nullptr && gzread != nullptr &&
179+
gzerror != nullptr,
180+
error_type_t::ValidationError,
181+
"Error loading zlib! Library version might be incompatible. Please decompress the .mps.gz file "
182+
"manually and open the uncompressed .mps file. Given path: %s",
183+
file.c_str());
184+
std::unique_ptr<gzFile_s, GzCloseDeleter> gzfp{gzopen(file.c_str(), "rb"), {gzclose_r}};
185+
mps_parser_expects(gzfp != nullptr,
186+
error_type_t::ValidationError,
187+
"Error opening compressed MPS file! Given path: %s",
188+
file.c_str());
189+
int zlib_status = gzbuffer(gzfp.get(), 1 << 20); // 1 MiB
190+
mps_parser_expects(zlib_status == Z_OK,
191+
error_type_t::ValidationError,
192+
"Could not set zlib internal buffer size for decompression! Given path: %s",
193+
file.c_str());
194+
std::vector<char> buf;
195+
const size_t readbufsize = 1ull << 24; // 16MiB
196+
std::vector<char> readbuf(readbufsize);
197+
int bytes_read = -1;
198+
while (bytes_read != 0) {
199+
bytes_read = gzread(gzfp.get(), readbuf.data(), readbuf.size());
200+
if (bytes_read > 0) { buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read); }
201+
if (bytes_read < 0) {
202+
gzerror(gzfp.get(), &zlib_status);
203+
break;
204+
}
205+
}
206+
buf.push_back('\0');
207+
mps_parser_expects(zlib_status == Z_OK,
208+
error_type_t::ValidationError,
209+
"Error in zlib decompression of MPS file! Given path: %s",
210+
file.c_str());
211+
return buf;
212+
}
213+
} // end namespace
214+
#endif // MPS_PARSER_WITH_ZLIB
215+
32216
namespace cuopt::mps_parser {
33217

34218
template <typename i_t>
@@ -342,6 +526,18 @@ std::vector<char> mps_parser_t<i_t, f_t>::file_to_string(const std::string& file
342526
{
343527
// raft::common::nvtx::range fun_scope("file to string");
344528

529+
#ifdef MPS_PARSER_WITH_BZIP2
530+
if (file.size() > 4 && file.substr(file.size() - 4, 4) == ".bz2") {
531+
return bz2_file_to_string(file);
532+
}
533+
#endif // MPS_PARSER_WITH_BZIP2
534+
535+
#ifdef MPS_PARSER_WITH_ZLIB
536+
if (file.size() > 3 && file.substr(file.size() - 3, 3) == ".gz") {
537+
return zlib_file_to_string(file);
538+
}
539+
#endif // MPS_PARSER_WITH_ZLIB
540+
345541
// Faster than using C++ I/O
346542
FILE* fp = fopen(file.c_str(), "r");
347543
mps_parser_expects(fp != nullptr,

cpp/libmps_parser/src/mps_parser.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ class mps_parser_t {
145145
std::unordered_set<i_t> bounds_defined_for_var_id{};
146146
static constexpr f_t unset_range_value = std::numeric_limits<f_t>::infinity();
147147

148-
/* Reads the equation from the input text file which is MPS formatted
148+
/* Reads an MPS input file into a buffer.
149149
*
150-
* Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more
151-
* details on this format.
150+
* If the file has a .gz or .bz2 suffix and zlib or libbzip2 are installed, respectively,
151+
* the function directly reads and decompresses the compressed MPS file.
152152
*/
153153
std::vector<char> file_to_string(const std::string& file);
154154
void fill_problem(mps_data_model_t<i_t, f_t>& problem);

0 commit comments

Comments
 (0)