Skip to content

Commit 5060304

Browse files
build: add the CUDA-free cuopt_client library
With the host code now separable, this adds the target that makes use of it. cuopt_client holds the host-side problem representation (parsers, data_model_view, mps_data_model, writers), the gRPC wire protocol (generated protos + mappers) and the LP/MIP gRPC client. libcuopt and cuopt_grpc_server both link it, so there is one mapper implementation rather than a client-side fork. Built as an OBJECT library plus a SHARED library, mirroring cuopt_objs/cuopt. cuopt_static embeds the objects directly rather than linking the shared library, because the internal test binaries reach parser internals such as mps_phase_registry_t. Three constraints are worth knowing, because each of them produced a bug during development that only surfaced at runtime: * The routing gRPC arm stays in libcuopt. Its mappers contain no raft/rmm/thrust, but they call routing::solver_settings_t and routing::assignment_t accessors that live in CUDA translation units -- so putting them in cuopt_client creates a cycle (libcuopt -> cuopt_client -> libcuopt). ldd -r does not flag it, because both libraries are always loaded together; it surfaces only when the call happens, as "undefined symbol: routing::solver_settings_t::get_time_limit". Moving that arm down needs its host-only accessors split out first, exactly as was done for LP/MIP. * solver_settings.cpp deliberately does not use `template class`. That instantiates every member, including the constructor, which builds a pdlp_solver_settings_t holding a pdlp_warm_start_data_t by value -- whose ctor is CUDA-side. Members are instantiated individually instead, and the constructor itself moved to solver_settings_gpu.cu, so the client library references nothing it cannot resolve. * cuopt_client uses default visibility, unlike cuopt_objs. libcuopt depends on roughly 214 of its symbols -- essentially the whole host-side API -- because this library was carved out of the internals rather than designed as a curated CUOPT_EXPORT surface. Hiding them makes libcuopt.so fail to load. Also moves logger.cpp into the client sources: both the parsers and the gRPC code include <utilities/logger.hpp>. Result: libcuopt_client.so NEEDED: libgrpc++, libprotobuf, libabseil, librapids_logger, libc, libstdc++, libgomp, libdl -- no libcudart, no rmm, no raft, no cudss undefined cuopt symbols: 0 Note that raft/rmm headers are still needed at *build* time (the CPU headers transitively include them), which costs nothing at runtime because the device getters throw. Separating the headers is follow-up work, and is what a standalone client package would additionally require. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
1 parent a9b22e9 commit 5060304

8 files changed

Lines changed: 419 additions & 194 deletions

File tree

cpp/CMakeLists.txt

Lines changed: 154 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,12 @@ if (BUILD_TESTS)
550550
endif ()
551551

552552
set(CUOPT_SRC_FILES)
553+
set(CUOPT_CLIENT_SRC_FILES)
553554
set(MPS_FAST_SRC_FILES)
554555
add_subdirectory(src)
555556

556557
if (HOST_LINEINFO)
557-
set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1")
558+
set_source_files_properties(${CUOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1")
558559
endif ()
559560

560561
# Needed for the fast MPS parser, available on all x86-64-v3 compliant x86 CPUs (essentially since Haswell ~2013)
@@ -567,11 +568,11 @@ endif ()
567568
# TODO: figure out a set of flags for ARM that fits the range of CPUs we wish to support (neoverse?)
568569
# NEON should be universal on aarch64 and enough for our purposes (parsing) though
569570

570-
# Apply -UNDEBUG only to solver source files (not gRPC infrastructure).
571-
# Must happen before gRPC files are appended to CUOPT_SRC_FILES.
571+
# Apply -UNDEBUG only to solver and parser source files (not gRPC infrastructure).
572+
# Must happen before gRPC files are appended to CUOPT_CLIENT_SRC_FILES.
572573
# Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO).
573574
if (DEFINE_ASSERT)
574-
set_property(SOURCE ${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
575+
set_property(SOURCE ${CUOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
575576
APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG")
576577
endif ()
577578

@@ -598,9 +599,7 @@ if (NOT SKIP_GRPC_BUILD)
598599
src/grpc/client/grpc_client.cpp
599600
src/grpc/client/grpc_client_env.cpp
600601
src/grpc/client/cython_grpc_client.cpp
601-
src/grpc/client/solve_remote.cpp
602602
)
603-
604603
# Routing (VRP) arm: everything that depends on the routing engine. Kept as
605604
# its own list so a routing-only gRPC client can be split out of the
606605
# cuopt_grpc component without moving code around again.
@@ -616,7 +615,24 @@ if (NOT SKIP_GRPC_BUILD)
616615
if (CUOPT_ENABLE_GRPC_ROUTING)
617616
list(APPEND GRPC_INFRA_FILES ${GRPC_ROUTING_FILES})
618617
endif ()
619-
list(APPEND CUOPT_SRC_FILES ${GRPC_INFRA_FILES})
618+
619+
# The protocol and the LP/MIP arm build into cuopt_client. The routing arm does NOT,
620+
# despite also being free of raft/rmm/thrust: its mappers call routing::solver_settings_t
621+
# and routing::assignment_t accessors that live in CUDA translation units inside libcuopt.
622+
# Putting them in cuopt_client creates a cycle (libcuopt -> cuopt_client -> libcuopt) that
623+
# ldd -r does not flag, because both libraries are loaded together -- it surfaces only when
624+
# the call happens, as "undefined symbol: routing::solver_settings_t::get_time_limit".
625+
# Moving the routing arm down needs those host-only accessors split out first, exactly as
626+
# was done for the LP/MIP settings.
627+
list(APPEND CUOPT_CLIENT_SRC_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES})
628+
if (CUOPT_ENABLE_GRPC_ROUTING)
629+
list(APPEND CUOPT_SRC_FILES ${GRPC_ROUTING_FILES})
630+
endif ()
631+
632+
# solve_remote.cpp is the local-vs-remote dispatcher: it calls into the GPU
633+
# solver, so it stays in cuopt_objs rather than moving down to cuopt_client.
634+
list(APPEND CUOPT_SRC_FILES src/grpc/client/solve_remote.cpp)
635+
list(APPEND GRPC_INFRA_FILES src/grpc/client/solve_remote.cpp)
620636

621637
# Always keep NDEBUG defined for gRPC infrastructure files so that abseil
622638
# headers inline Mutex::Dtor() instead of emitting an external call.
@@ -631,6 +647,127 @@ if (NOT SKIP_GRPC_BUILD)
631647
APPEND PROPERTY COMPILE_OPTIONS "$<$<COMPILE_LANGUAGE:CXX>:-fvisibility=default>")
632648
endif (NOT SKIP_GRPC_BUILD)
633649

650+
# ##################################################################################################
651+
# - cuopt_client - CPU-only support library ----------------------------------------------------------
652+
#
653+
# Holds the host-side problem representation (parsers, data_model_view, mps_data_model,
654+
# writers), the gRPC wire protocol (generated protos + mappers), and the gRPC client.
655+
# None of it touches CUDA, so this library links no CUDA runtime.
656+
#
657+
# It exists so the Python extension modules that never call into the GPU -- data_model,
658+
# solver_settings, io, and the gRPC client -- can link something other than libcuopt.so,
659+
# which is what makes a GPU-free client install possible. libcuopt and cuopt_grpc_server
660+
# both link it, so there is exactly one implementation of the mappers, not a client fork.
661+
#
662+
# LANGUAGES is deliberately not CUDA here: adding a .cu file to CUOPT_CLIENT_SRC_FILES
663+
# should fail loudly rather than quietly reintroduce a CUDA dependency.
664+
# Built as an OBJECT library first, mirroring cuopt_objs/cuopt. The shared library below
665+
# keeps hidden visibility and exports only the curated CUOPT_EXPORT surface, while
666+
# cuopt_static (for internal tests) links the objects directly -- internal symbols such as
667+
# the fast MPS parser's mps_phase_registry_t are not exported, and the internal test
668+
# binaries need them.
669+
add_library(cuopt_client_objs OBJECT ${CUOPT_CLIENT_SRC_FILES})
670+
# NOTE: default visibility, deliberately unlike cuopt_objs.
671+
#
672+
# cuopt_objs can hide everything not marked CUOPT_EXPORT because libcuopt has a curated
673+
# public C++ API. cuopt_client is different: it was carved out of the *internals*, so
674+
# libcuopt itself depends on ~214 of its symbols (the whole cpu_optimization_problem_t /
675+
# data_model_view_t / mps_data_model_t / grpc_client_t surface). Those are internal
676+
# cross-library references, not a public API, and hiding them makes libcuopt.so fail to
677+
# load with e.g. "undefined symbol: grpc_client_t::solve_mip".
678+
#
679+
# Curating them behind CUOPT_EXPORT would mean annotating essentially every host-side
680+
# method, so default visibility is the right trade here.
681+
set_target_properties(cuopt_client_objs
682+
PROPERTIES POSITION_INDEPENDENT_CODE ON
683+
CXX_SCAN_FOR_MODULES OFF
684+
)
685+
686+
add_library(cuopt_client SHARED $<TARGET_OBJECTS:cuopt_client_objs>)
687+
add_library(cuopt::cuopt_client ALIAS cuopt_client)
688+
689+
target_include_directories(cuopt_client
690+
PUBLIC
691+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
692+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>"
693+
INTERFACE
694+
"$<INSTALL_INTERFACE:include>"
695+
)
696+
697+
target_compile_definitions(cuopt_client
698+
PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}"
699+
)
700+
701+
set_target_properties(cuopt_client
702+
PROPERTIES POSITION_INDEPENDENT_CODE ON
703+
CXX_SCAN_FOR_MODULES OFF
704+
BUILD_RPATH "\$ORIGIN"
705+
INSTALL_RPATH "\$ORIGIN"
706+
LINKER_LANGUAGE CXX
707+
)
708+
709+
target_compile_definitions(cuopt_client_objs
710+
PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}"
711+
)
712+
713+
target_compile_options(cuopt_client_objs
714+
PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CUOPT_CXX_FLAGS}>"
715+
)
716+
717+
target_include_directories(cuopt_client_objs
718+
PRIVATE
719+
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty"
720+
"${CMAKE_CURRENT_SOURCE_DIR}/src"
721+
"${CMAKE_CURRENT_SOURCE_DIR}/src/io"
722+
"${CMAKE_CURRENT_SOURCE_DIR}/src/grpc"
723+
"${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/client"
724+
"${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated"
725+
"${CMAKE_CURRENT_BINARY_DIR}"
726+
"${CMAKE_CURRENT_BINARY_DIR}/include"
727+
$<$<BOOL:${CUOPT_PARSER_WITH_BZIP2}>:${BZIP2_INCLUDE_DIRS}>
728+
$<$<BOOL:${CUOPT_PARSER_WITH_ZLIB}>:${ZLIB_INCLUDE_DIRS}>
729+
PUBLIC
730+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
731+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>"
732+
INTERFACE
733+
"$<INSTALL_INTERFACE:include>"
734+
)
735+
736+
# CCCL is a compile-time (header-only) dependency here: the fast MPS parser uses
737+
# host helpers from <cuda/cmath> (ceil_div, round_up). It pulls in no CUDA runtime.
738+
# bzip2 / zlib / lz4 are dlopen'd at runtime by file_to_string.cpp, so they are
739+
# header-only here too and deliberately absent from the link line.
740+
# The OBJECT library needs these for their INTERFACE include dirs / defines at compile time.
741+
target_link_libraries(cuopt_client_objs
742+
PUBLIC
743+
rapids_logger::rapids_logger
744+
CCCL::CCCL
745+
# Header-only here, exactly like CCCL: the client sources transitively include
746+
# <rmm/device_uvector.hpp> and <raft/core/device_span.hpp> via pdlp/solver_settings.hpp.
747+
# No CUDA runtime is linked -- the device getters those headers declare only ever
748+
# throw -- but without these the build relies on conda happening to put the headers on
749+
# the default include path, and a CPM/fetched-rmm build fails to find them.
750+
rmm::rmm
751+
raft::raft
752+
PRIVATE
753+
simde::simde
754+
OpenMP::OpenMP_CXX
755+
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:protobuf::libprotobuf>
756+
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:gRPC::grpc++>
757+
)
758+
759+
target_link_libraries(cuopt_client
760+
PUBLIC
761+
rapids_logger::rapids_logger
762+
CCCL::CCCL
763+
PRIVATE
764+
simde::simde
765+
OpenMP::OpenMP_CXX
766+
${CMAKE_DL_LIBS}
767+
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:protobuf::libprotobuf>
768+
$<$<BOOL:${CUOPT_ENABLE_GRPC}>:gRPC::grpc++>
769+
)
770+
634771
add_library(cuopt_objs OBJECT
635772
${CUOPT_SRC_FILES}
636773
)
@@ -757,6 +894,7 @@ target_compile_definitions(cuopt_objs PUBLIC
757894

758895
target_link_libraries(cuopt_objs
759896
PUBLIC
897+
cuopt::cuopt_client
760898
CUDA::cublas
761899
CUDA::cusparse
762900
rmm::rmm
@@ -777,7 +915,10 @@ target_link_libraries(cuopt_objs
777915
# - generate tests --------------------------------------------------------------------------------
778916
if (BUILD_TESTS)
779917
include(CTest)
780-
add_library(cuopt_static STATIC $<TARGET_OBJECTS:cuopt_objs>)
918+
# Embeds cuopt_client_objs directly rather than linking libcuopt_client.so: the internal
919+
# test binaries reach parser internals that the shared library deliberately does not
920+
# export. Do not also link cuopt::cuopt_client here -- that would duplicate every symbol.
921+
add_library(cuopt_static STATIC $<TARGET_OBJECTS:cuopt_objs> $<TARGET_OBJECTS:cuopt_client_objs>)
781922
target_link_libraries(cuopt_static
782923
PUBLIC
783924
CUDA::cublas
@@ -839,6 +980,7 @@ target_include_directories(cuopt
839980
)
840981
target_link_libraries(cuopt
841982
PUBLIC
983+
cuopt::cuopt_client
842984
CUDA::cublas
843985
CUDA::cusparse
844986
rmm::rmm
@@ -904,14 +1046,14 @@ else ()
9041046
endif ()
9051047

9061048
# adds the .so files to the runtime deb package
907-
install(TARGETS cuopt
1049+
install(TARGETS cuopt cuopt_client
9081050
DESTINATION ${_LIB_DEST}
9091051
COMPONENT runtime
9101052
EXPORT cuopt-exports
9111053
)
9121054

9131055
# adds the .so files to the development deb package
914-
install(TARGETS cuopt
1056+
install(TARGETS cuopt cuopt_client
9151057
DESTINATION ${_LIB_DEST}
9161058
COMPONENT dev
9171059
)
@@ -939,7 +1081,7 @@ cuOpt library is a collection of GPU accelerated combinatorial optimization algo
9391081

9401082
rapids_export(INSTALL cuopt
9411083
EXPORT_SET cuopt-exports
942-
GLOBAL_TARGETS cuopt
1084+
GLOBAL_TARGETS cuopt cuopt_client
9431085
NAMESPACE cuopt::
9441086
DOCUMENTATION doc_string
9451087
)
@@ -948,7 +1090,7 @@ rapids_export(INSTALL cuopt
9481090
# - build export -------------------------------------------------------------------------------
9491091
rapids_export(BUILD cuopt
9501092
EXPORT_SET cuopt-exports
951-
GLOBAL_TARGETS cuopt
1093+
GLOBAL_TARGETS cuopt cuopt_client
9521094
NAMESPACE cuopt::
9531095
DOCUMENTATION doc_string
9541096
)

cpp/src/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
# cmake-format: on
55

66
set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/seed_generator.cu
7-
${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp
87
${CMAKE_CURRENT_SOURCE_DIR}/utilities/version_info.cpp
98
${CMAKE_CURRENT_SOURCE_DIR}/utilities/timestamp_utils.cpp
109
${CMAKE_CURRENT_SOURCE_DIR}/utilities/work_unit_scheduler.cpp)
1110

11+
# logger.cpp backs <utilities/logger.hpp>, which both the parsers and the gRPC
12+
# sources include, so it belongs to the CPU library. Without this, cuopt_client would
13+
# have an undefined reference to the logger.
14+
set(UTIL_CLIENT_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp)
15+
1216
add_subdirectory(linear_algebra)
1317
add_subdirectory(pdlp)
1418
add_subdirectory(math_optimization)
@@ -26,4 +30,5 @@ add_subdirectory(branch_and_bound)
2630
add_subdirectory(cuts)
2731

2832
set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${UTIL_SRC_FILES} PARENT_SCOPE)
33+
set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${UTIL_CLIENT_SRC_FILES} PARENT_SCOPE)
2934
set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE)

cpp/src/io/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,10 @@ set(PARSERS_SRC_FILES
2323
${MPS_FAST_SRC_FILES}
2424
)
2525

26-
set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE)
26+
# The parsers and the host-side problem representation are CUDA-free (the only
27+
# `cuda::` uses are host integer helpers from header-only libcu++), so they build
28+
# into the CPU-only cuopt_client library rather than into cuopt_objs. That is what
29+
# lets the Python data_model / solver_settings / io extension modules link a
30+
# library with no CUDA runtime dependency.
31+
set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE)
2732
set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE)

cpp/src/math_optimization/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55

66
list(PREPEND
77
MATH_OPT_SRC_FILES
8-
${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cpp
98
${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_gpu.cu
109
${CMAKE_CURRENT_SOURCE_DIR}/solution_reader.cu
1110
${CMAKE_CURRENT_SOURCE_DIR}/solution_writer.cu
1211
${CMAKE_CURRENT_SOURCE_DIR}/tic_toc.cpp
1312
)
1413

14+
# solver_settings_t is host-only apart from the device members in solver_settings_gpu.cu,
15+
# so the bulk of it builds into cuopt_client. The gRPC client takes a solver_settings_t
16+
# in its public API, so this is required for the client library to resolve standalone.
17+
set(MATH_OPT_CLIENT_SRC_FILES
18+
${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cpp
19+
)
20+
1521
set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES}
1622
${MATH_OPT_SRC_FILES} PARENT_SCOPE)
23+
set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES}
24+
${MATH_OPT_CLIENT_SRC_FILES} PARENT_SCOPE)

0 commit comments

Comments
 (0)