Skip to content

build(cmake): split libcuopt into cuopt_client / cuopt_mathopt / cuopt_routing component libs - #1622

Open
ramakrishnap-nv wants to merge 47 commits into
mainfrom
feat/split-routing-lp-libs
Open

build(cmake): split libcuopt into cuopt_client / cuopt_mathopt / cuopt_routing component libs#1622
ramakrishnap-nv wants to merge 47 commits into
mainfrom
feat/split-routing-lp-libs

Conversation

@ramakrishnap-nv

@ramakrishnap-nv ramakrishnap-nv commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Splits libcuopt.so into three component SHARED libraries — cuopt_client, cuopt_mathopt, cuopt_routing — and ships libcuopt.so as a GNU ld script naming them.

Packaging is unchanged: still one libcuopt wheel and one libcuopt conda package. This PR is the prerequisite for the per-solver package split tracked in #1635.

Consumer impact

-lcuopt keeps working, and CMake consumers are unaffected.

libcuopt.so is no longer an ELF object. It is a linker script:

INPUT(libcuopt_mathopt.so libcuopt_routing.so)

This is the mechanism glibc uses for libc.so. It is needed because ld does not resolve a consumer's undefined symbols through a dependency's DT_NEEDED (--no-copy-dt-needed-entries, the default since binutils 2.22), so a thin umbrella library would have forced every C consumer to name the components explicitly. With the script, gcc app.c -lcuopt links exactly as before. cuopt::cuopt is now an INTERFACE target, so find_package(cuopt) and the Cython modules are unchanged. cuopt_client is not named in the script because it arrives through the other components' DT_NEEDED.

Two consequences worth noting:

  • dlopen("libcuopt.so") no longer works — load the components instead. load.py does this.
  • Anything previously linked against libcuopt.so needs a rebuild, since its DT_NEEDED now names a file that is not ELF.

find_package(cuopt) exposes cuopt::cuopt plus cuopt::routing and cuopt::mathopt.

Components

Sizes are stripped. Every cross-library reference has a matching DT_NEEDED.

Component Size Contents Depends on
cuopt_client 2.4 MB host problem representation, parsers, wire protocol, LP/MIP and VRP gRPC clients, solve_remote nothing — leaf, no CUDA
cuopt_routing 26.5 MB VRP / routing engine cuopt_client (8 symbols)
cuopt_mathopt 43.8 MB LP / MIP / QP / SOCP engine, utilities cuopt_client (206 symbols)

Neither engine has a DT_NEEDED on the other, which is what makes the per-solver package split possible. libcuopt_client.so has no CUDA, rmm or raft in NEEDED and no undefined cuopt:: symbols, so it resolves standalone — that is what makes a GPU-free client install possible (#1872).

Binaries: cuopt_cli needs client and mathopt only (0 routing symbols); cuopt_grpc_server needs all three. #1635 records where each lands.

Components that were tried and removed

Both were dropped after measuring the built libraries rather than reading the CMake.

cuopt_base held shared utilities, on the assumption that both engines resolved process-global singletons from it. libcuopt_mathopt.so referenced 9 symbols from it; routing, grpc, client, cuopt_cli and cuopt_grpc_server referenced none, and routing carried no DT_NEEDED on it at all. Its rationale had also eroded: the logger became header-only per component in #1778, linear algebra moved into cuopt_mathopt after review, and #1809 replaced the process-wide seed generator with per-worker RNGs. Four utility files shipped as a 1 MB library for one caller, so they now build into cuopt_mathopt.

cuopt_grpc held the gRPC bridge. Once #1884 moved the routing arm into cuopt_client, and solve_remote.cpp moved there too, what remained was 18 KB holding one source file: an ELF constructor calling register_remote_solvers. It exported no cuOpt symbols — its 11 dynamic symbols were incidental raft and libstdc++ template instantiations leaked from headers. As a shipped package it would have been a wheel and a conda output containing a constructor and nothing callable.

Remote-solve dispatch, simplified

Review raised that the atomic function pointers, the constructor registration and the dlopen should not be needed. They existed to break a cycle: solve_lp_remote and solve_mip_remote lived in the gRPC component, which depended on math-opt, so math-opt could not call them.

That cycle is gone. Both entry points now live in cuopt_client, which is a leaf, and cuopt_mathopt links it — the dependency is one-way. So both call sites became direct calls, and remote_solve_registry.{hpp,cpp}, grpc_registration.cpp, both std::atomic function pointers, the ready flag, register_remote_solvers and ensure_remote_solvers_loaded are all deleted. Net -163 lines.

The one case the registry handled was a build without gRPC, where the remote entry points do not exist and the null pointer degraded to a runtime error. That is now a compile-time guard on CUOPT_ENABLE_GRPC, with the same error text preserved.

This also removes solve_lp_remote_fn_t and solve_mip_remote_fn_t, which a reviewer asked to unify — there is now no type to unify.

Also in this PR

  • WRITE_FATBIN applied fatbin.ld to the umbrella, which held no device code after the split — the section grouping had silently become a no-op. It now applies to the components that carry the fatbins.
  • conda prefix_detection.ignore listed only libcuopt.so, which is no longer a binary; it now lists the components.
  • The umbrella translation unit and its --as-needed anchor symbols are gone entirely — the linker script makes them unnecessary.
  • cuopt_routing referenced 8 symbols from cuopt_client with no DT_NEEDED, resolving only because something else had pulled the client in. A routing-only install would have failed at load; the link is now explicit.

Testing

Full build clean, no undefined references. Remote execution verified end to end against a forked server: CpuOnlyWithServerTest.lp_solve and .mip_solve pass, exercising the new direct dispatch through the C API.

Verified separately: bare -lcuopt links without --allow-shlib-undefined and the resulting binary runs; find_package(cuopt) resolves the targets; load.py loads all components and the C API resolves.

cuopt_client links librmm

libcuopt_client.so links librmm for two symbols it never meaningfully uses: cuda_stream_view's constructor, emitted by static initializers rmm's header creates in every including translation unit for globals the client never names, and device_buffer's destructor, instantiated through the std::variant in linear_programming_ret_t whose GPU alternative the remote client never constructs.

cuopt_client_objs already listed rmm::rmm, but an OBJECT library does not propagate its link interface to a target built from $<TARGET_OBJECTS:...>, so the shared library carried two undefined rmm symbols and no DT_NEEDED. That resolved by accident while libcuopt.so was a real ELF linking rmm; once it became a linker script, a Cython extension loading the client directly failed to import.

This does not reintroduce CUDA: librmm.so is 1.5 MB with no CUDA in its own NEEDED, and its only CUDA-ish dependency is cuda-version, a 21.6 KB metapackage with no files. A GPU-free client install still works. Removing the need entirely is tracked in #1890.

Remote log callback

CpuOnlyWithServerTest.log_callback_remote failed on every conda-cpp-tests config: the remote solve succeeded but no streamed log lines reached the callback. Fixed here, and #1878 closed.

The components are built with hidden visibility, so each got its own copy of the inline thread_local holding the registration. cuOptSetLogCallback lives in cuopt_c.cpp, which builds into cuopt_mathopt, while the remote-solve log forwarding reads it from solve_remote.cpp in cuopt_client — two copies, so the remote path saw no callback. Marking it CUOPT_EXPORT lets the dynamic linker collapse them into one. Registration remains scoped per thread, which is what keeps concurrent solves from capturing each other's callback.

A split-only bug: it passes on a monolithic build, which is why it survived until libcuopt.so became a linker script.

Next steps (#1635)

  1. Relink the Cython extension modules — all nine link cuopt::cuopt. Until they link their actual component, every extension pulls the full graph.
  2. Per-component install components and header installation — both are monolithic today.
  3. Split the wheels and conda recipeslibcuopt-client / -mathopt / -routing plus a libcuopt metapackage, and cuopt-grpc-server for the server binary.

ramakrishnap-nv and others added 3 commits July 24, 2026 13:48
…lp + umbrella

Introduce three STATIC component libraries that logically partition the
cuOpt sources by domain, then fold them into the existing libcuopt.so
umbrella via --whole-archive (LINK_LIBRARY:WHOLE_ARCHIVE).

Component libraries:
- cuopt_base   — utilities + linear algebra (logger, work scheduler)
- cuopt_routing — VRP / routing engine; links cuopt_base
- cuopt_lp      — LP / MIP / numerical optimization; links cuopt_base

Umbrella:
- cuopt SHARED  — re-exports all symbols from the three statics via
  --whole-archive; backward-compatible for GAMS (-lcuopt / libcuopt.so)

CMake aliases exposed: cuopt::base, cuopt::routing, cuopt::lp, cuopt::cuopt

No source files moved. External build output (libcuopt.so, headers,
install layout) is unchanged. SKIP_ROUTING_BUILD=ON continues to work
by omitting cuopt_routing from the build and umbrella link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Four issues found while validating the library split locally:

- cuopt_routing was missing OpenMP::OpenMP_CUDA, causing routing CUDA
  files to reject #pragma omp directives as unknown in CUDA compiler mode
- cuopt_lp was missing simde::simde, required by the fast MPS parser
  (io/experimental_mps_fast/) which uses SIMD intrinsics via simde headers
- The umbrella cuopt target was missing src/io in its private include
  dirs, causing gRPC mapper files (grpc_problem_mapper.cpp) that include
  mps_parser_internal.hpp to fail to compile
- WHOLE_ARCHIVE linkage on the umbrella was PUBLIC, propagating the static
  sub-libs as link dependencies to all consumers (test binaries). This
  caused double-definition errors when tests linked both libcuopt.so and
  the statics. Changed to PRIVATE and re-exposed the statics' transitive
  PUBLIC deps (rmm, raft, CCCL, CUDA libs) directly on the umbrella so
  that consumers receive the correct source-fetched include dirs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Add libcuopt_base.a, libcuopt_routing.a, and libcuopt_lp.a to the
package_contents file check so CI fails fast if any of the three
component static libraries are missing from the installed package.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

ramakrishnap-nv and others added 2 commits July 27, 2026 10:08
…to cuopt_lp

Component libs (cuopt_base, cuopt_routing, cuopt_lp) are now SHARED
instead of STATIC. The umbrella libcuopt.so becomes a thin stub (~15 KB)
carrying only DT_NEEDED entries for the three component libs; no code or
WHOLE_ARCHIVE baking.

The gRPC bridge (mapper + Cython client) moves from the umbrella into
cuopt_lp where it semantically belongs — LP/MIP remote solve is an LP
concern. The umbrella drops all gRPC sources, include dirs, and
protobuf/gRPC link deps.

Both RPATH settings use $ORIGIN so component libs find each other when
co-installed. Conda package_contents check updated from .a to .so.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Moves all gRPC infrastructure (proto mappers, Cython client, solve_remote)
from cuopt_lp into a new cuopt_grpc SHARED component. cuopt_grpc links
cuopt_lp + cuopt_routing (when built), keeping both core solver libs free
of any gRPC/protobuf dependency.

The grpc_server binary now links cuopt_grpc directly. The umbrella links
cuopt_grpc when gRPC is built so -lcuopt continues to expose remote-solve
symbols to existing consumers.

When PR #1597 (VRP gRPC) lands, routing gRPC sources go into cuopt_grpc
alongside the LP ones — no cross-dependency between cuopt_lp and cuopt_routing
is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv ramakrishnap-nv self-assigned this Jul 27, 2026
@ramakrishnap-nv ramakrishnap-nv added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels Jul 27, 2026
@ramakrishnap-nv ramakrishnap-nv added this to the 26.10 milestone Jul 27, 2026
Resolve conflicts between:
- Our component library split (cuopt_base/routing/lp/grpc as SHARED + thin umbrella)
- main's cuopt_objs OBJECT library approach added in #1581

Both coexist: cuopt_objs + cuopt_static serve internal tests; the SHARED
component libs + umbrella serve all other consumers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test ec81ebc

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

cuOpt componentized build and remote execution

Layer / File(s) Summary
Component source aggregation
cpp/src/.../CMakeLists.txt
CMake propagates nested source lists and aggregates base, routing, and mathematical-optimization sources.
Component libraries and dependencies
cpp/CMakeLists.txt, cpp/cmake/umbrella.cpp.in
Shared component targets, dependencies, umbrella linkage, installation exports, and executable linkage are updated.
Dynamic remote-solver registration
cpp/include/.../remote_solve_registry.hpp, cpp/src/grpc/..., cpp/src/pdlp/..., cpp/src/mip_heuristics/solve.cu, cpp/cuopt_cli.cpp
Remote LP and MIP execution uses registered callbacks and loads gRPC on demand.
Distribution and examples
ci/*, conda/recipes/libcuopt/recipe.yaml, python/libcuopt/CMakeLists.txt, docs/cuopt/...
Packaging, RPATHs, C example linking, and documentation link checking are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • NVIDIA/cuopt#1625: Both changes update shared-library target construction, linkage, and symbol/export behavior.
  • NVIDIA/cuopt#1683: Both changes update CMake source aggregation for the MIP build.

Suggested reviewers: tmckayus, bdice, akifcorduk, aliceb-nv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description directly explains the component-library split, umbrella library behavior, consumer impact, remote-solve changes, packaging, testing, and follow-up work.
Title check ✅ Passed The title clearly identifies the primary change: splitting libcuopt into client, mathematical-optimization, and routing component libraries.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/split-routing-lp-libs
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/split-routing-lp-libs

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/CMakeLists.txt (1)

902-918: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Exported component names should match the documented API

cuopt::base / cuopt::lp are build-tree aliases only. The install/export set will expose the real targets (cuopt::cuopt_base, cuopt::cuopt_lp, etc.) unless those targets set EXPORT_NAME, so a consumer using find_package(cuopt) won’t be able to link against cuopt::base as documented.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/CMakeLists.txt` around lines 902 - 918, Update the component target
export configuration used by the rapids_export INSTALL and BUILD calls so
installed targets retain the documented names cuopt::base, cuopt::routing,
cuopt::lp, and cuopt::grpc. Set the appropriate EXPORT_NAME values on the
underlying cuopt_component targets, while preserving cuopt::cuopt as the
umbrella target and keeping build-tree aliases consistent.
🧹 Nitpick comments (2)
cpp/CMakeLists.txt (2)

549-556: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Give the git hash lookup a fallback for non-git source trees.

Without RESULT_VARIABLE/ERROR_QUIET, tarball builds leak git's error to the configure log and bake an empty hash into build_info.hpp.

♻️ Proposed fallback
 execute_process(
         COMMAND git rev-parse --short HEAD
         WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
         OUTPUT_VARIABLE GIT_COMMIT_HASH
         OUTPUT_STRIP_TRAILING_WHITESPACE
+        RESULT_VARIABLE _git_hash_result
+        ERROR_QUIET
 )
+if(NOT _git_hash_result EQUAL 0 OR GIT_COMMIT_HASH STREQUAL "")
+    set(GIT_COMMIT_HASH "unknown")
+endif()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/CMakeLists.txt` around lines 549 - 556, Update the git hash lookup in the
top-level CMake configuration to capture the execute_process result and suppress
stderr for source trees without Git metadata. When the lookup fails, assign a
stable non-empty fallback hash before the existing GIT_COMMIT_HASH message and
build_info.hpp generation; preserve the real short HEAD value for Git checkouts.

664-675: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

cuopt_objs duplicates the component include/definition setup.

The object library re-declares papilo/pslp/dejavu includes, CUDSS defines, and architecture defines that cuopt_configure_component (plus cuopt_lp) already establish. Since cuopt_objs backs cuopt_static for the test builds, drift here means tests compile under a different configuration than shipped libraries. Consider factoring the shared include/definition block into a helper both paths call.

Also applies to: 693-694, 720-724

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/CMakeLists.txt` around lines 664 - 675, Refactor the shared include
directories and compile definitions currently duplicated by cuopt_objs and the
cuopt_configure_component/cuopt_lp setup into a reusable CMake helper. Invoke
that helper for both cuopt_objs and the shipped-library path, including the
papilo/pslp/dejavu includes, CUDSS definitions, and architecture definitions,
while preserving target-specific settings such as POSITION_INDEPENDENT_CODE and
logging definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cpp/CMakeLists.txt`:
- Around line 902-918: Update the component target export configuration used by
the rapids_export INSTALL and BUILD calls so installed targets retain the
documented names cuopt::base, cuopt::routing, cuopt::lp, and cuopt::grpc. Set
the appropriate EXPORT_NAME values on the underlying cuopt_component targets,
while preserving cuopt::cuopt as the umbrella target and keeping build-tree
aliases consistent.

---

Nitpick comments:
In `@cpp/CMakeLists.txt`:
- Around line 549-556: Update the git hash lookup in the top-level CMake
configuration to capture the execute_process result and suppress stderr for
source trees without Git metadata. When the lookup fails, assign a stable
non-empty fallback hash before the existing GIT_COMMIT_HASH message and
build_info.hpp generation; preserve the real short HEAD value for Git checkouts.
- Around line 664-675: Refactor the shared include directories and compile
definitions currently duplicated by cuopt_objs and the
cuopt_configure_component/cuopt_lp setup into a reusable CMake helper. Invoke
that helper for both cuopt_objs and the shipped-library path, including the
papilo/pslp/dejavu includes, CUDSS definitions, and architecture definitions,
while preserving target-specific settings such as POSITION_INDEPENDENT_CODE and
logging definitions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2d788dd1-855a-4038-900e-9a3eab8f07be

📥 Commits

Reviewing files that changed from the base of the PR and between 91134a2 and ec81ebc.

📒 Files selected for processing (13)
  • conda/recipes/libcuopt/recipe.yaml
  • cpp/CMakeLists.txt
  • cpp/src/CMakeLists.txt
  • cpp/src/barrier/CMakeLists.txt
  • cpp/src/branch_and_bound/CMakeLists.txt
  • cpp/src/cuts/CMakeLists.txt
  • cpp/src/dual_simplex/CMakeLists.txt
  • cpp/src/io/CMakeLists.txt
  • cpp/src/linear_algebra/CMakeLists.txt
  • cpp/src/math_optimization/CMakeLists.txt
  • cpp/src/mip_heuristics/CMakeLists.txt
  • cpp/src/pdlp/CMakeLists.txt
  • cpp/src/routing/CMakeLists.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test a54e62a

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

CI Test Summary

✅ All 31 test job(s) passed.

ramakrishnap-nv and others added 2 commits July 27, 2026 16:23
libcuopt.so is now a thin umbrella with DT_NEEDED on libcuopt_base.so,
libcuopt_routing.so, libcuopt_lp.so, and libcuopt_grpc.so. auditwheel
traverses DT_NEEDED transitively and failed when it couldn't locate
the component libs. Exclude them the same way libcuopt.so is excluded —
they ship with the libcuopt wheel and are available at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…builds

main appends GRPC_INFRA_FILES to CUOPT_SRC_FILES before creating cuopt_objs
so cuopt_static (used by NUMOPT_INTERNAL_TEST) gets solve_lp_remote /
solve_mip_remote. We dropped that line when we moved those files into
cuopt_grpc, causing undefined-reference link failures in tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test 705167f

ramakrishnap-nv and others added 3 commits July 28, 2026 10:40
…lution

cuopt_lp.so calls solve_lp/mip_remote (under CUOPT_ENABLE_GRPC) which are
defined in cuopt_grpc.so. With --as-needed the linker was dropping
libcuopt_grpc.so from executables that never directly referenced a grpc
symbol, leaving solve_lp_remote unresolved at runtime.

Route remote solves in cuopt_cli directly through solve_lp/mip_remote so
libcuopt_grpc.so is a genuine DT_NEEDED of the binary; --as-needed then
keeps it in the link and the symbol is in scope when libcuopt_lp.so needs it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
memory_backend_t::CPU fires on any CPU-only host even when
CUOPT_REMOTE_HOST is not set, incorrectly routing local solves
through the gRPC client path. is_remote_execution_enabled() checks
CUOPT_REMOTE_HOST + CUOPT_REMOTE_PORT and is the correct guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Conflicts resolved in cpp/CMakeLists.txt:

- cuopt_objs: take KaMinPar include-dirs, compile-defs, and dependency
  from main; drop duplicate variable definitions already hoisted to the
  top of the file by the split PR (CUOPT_PRIVATE_CUDA_LIBS, git hash,
  build_info.hpp, JOINED_CUDA_ARCHITECTURES, CUDSS_MT_LIB_FILE_NAME).

- cuopt (umbrella): keep HEAD (empty) — the thin umbrella does not need
  direct CUDA/rmm/PSLP/KaMinPar links; those live in the component libs.
  KaMinPar is added to cuopt_lp (shared lib that compiles partitioner.cpp)
  separately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv ramakrishnap-nv added the do not merge Do not merge if this flag is set label Jul 28, 2026
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test 1bc38f7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test 8511bd0

ramakrishnap-nv added a commit that referenced this pull request Sep 9, 2026
Review feedback. The previous diagram drew cuopt_grpc_server and libcuopt
as one box, which hid the fact that libcuopt.so is the shared foundation:
both the cuopt Python extension modules and cuopt_grpc_server link it, and
so does cuopt_cli. That convergence is the thing #1622 splits and #1804
carves from, so it should be visible.

libcuopt.so is now its own box with both link edges arriving at it, and the
gRPC arrow terminates at cuopt_grpc_server rather than trailing off toward
the library, which misrepresented what a gRPC client actually talks to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
…-libs

# Conflicts:
#	ci/build_wheel_cuopt.sh
#	cpp/CMakeLists.txt
@ramakrishnap-nv
ramakrishnap-nv requested a review from a team as a code owner September 9, 2026 20:23
cuopt_base had one consumer. libcuopt_mathopt.so referenced 9 symbols from
it -- seed_generator::seed_, print_version_info, six work_unit_scheduler_t
methods and printTimestamp -- while libcuopt_routing.so, libcuopt_grpc.so,
libcuopt_client.so, cuopt_cli and cuopt_grpc_server referenced none, and
routing carried no DT_NEEDED on it at all.

Its rationale had eroded from three directions since it was written. The
logger became header-only per component in #1778, linear algebra moved into
cuopt_mathopt after review, and #1809 replaced the process-wide seed
generator with per-worker RNGs. What remained was UTIL_SRC_FILES, four
utility files, shipped as a 1 MB shared library for a single caller.

The argument for a separate library is that duplicating stateful code into
two components gives two copies of the global state. That no longer applies
when only one component uses it.

UTIL_SRC_FILES now builds into cuopt_mathopt. Since routing inherited
rmm/raft/CCCL/cublas/cusparse transitively through cuopt_base's PUBLIC link
interface, both components now declare those explicitly, and the
architecture defines that version_info.cpp reports move to cuopt_mathopt.

Also drops the component from the linker script, the umbrella INTERFACE
lists, the fatbin targets, the export set, the conda recipe, the wheel
exclusions and load.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv ramakrishnap-nv changed the title build(cmake): split libcuopt into cuopt_base / cuopt_routing / cuopt_lp component libs build(cmake): split libcuopt into cuopt_routing / cuopt_mathopt / cuopt_grpc component libs Sep 9, 2026
…-libs

# Conflicts:
#	ci/build_wheel_cuopt.sh
#	conda/recipes/libcuopt/recipe.yaml
#	cpp/CMakeLists.txt
#	cpp/src/CMakeLists.txt
#	cpp/src/io/CMakeLists.txt
#	cpp/src/math_optimization/CMakeLists.txt
#	cpp/src/mip_heuristics/CMakeLists.txt
#	cpp/src/pdlp/CMakeLists.txt
#	python/libcuopt/CMakeLists.txt
@ramakrishnap-nv ramakrishnap-nv changed the title build(cmake): split libcuopt into cuopt_routing / cuopt_mathopt / cuopt_grpc component libs build(cmake): split libcuopt into cuopt_client / cuopt_mathopt / cuopt_routing / cuopt_grpc component libs Sep 10, 2026
ramakrishnap-nv and others added 6 commits September 10, 2026 10:37
The routing gRPC mappers were the last thing keeping a GPU-free client from
covering VRP. They stayed out of cuopt_client because they reached into the
routing engine, but the reach turned out to be shallow: 14 symbols, all
trivial host-only accessors that happen to sit in CUDA translation units.

  grpc_routing_settings_mapper    8  routing::solver_settings_t getters/setters
  grpc_routing_solution_mapper    6  routing::assignment_t getters
  grpc_routing_problem_mapper     0
  grpc_client_vrp                 0
  cython_grpc_client_vrp          0

The VRP client itself needed nothing from the engine.

routing/solver_settings.cu is renamed to .cpp -- the whole file was already
host code, plain accessors over scalar members, and its header pulls in no
CUDA. The six assignment_t accessors move to assignment_accessors.cpp and are
instantiated per member rather than with `template class`, which would also
instantiate the device-facing members and pull CUDA back in. This is the same
split #1801 through #1803 applied to the LP/MIP settings, and the approach
#1804 anticipated for routing.

libcuopt_client.so grows 2.3 MB to 2.4 MB and keeps its defining properties:
no CUDA, rmm or raft in NEEDED, no undefined cuopt:: symbols, and no
DT_NEEDED on any other cuOpt library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
The routing gRPC arm had almost no C++ coverage. GRPC_INTEGRATION_TEST held
no routing cases at all, and of the two mappers that read the accessors this
branch moves, neither had a test -- only the problem mapper did, and it is
the one that touches no accessors.

Adds two tests that catch different things.

DefaultServerTests.SolveVRP follows the same shape as the LP and MIP cases in
that fixture: submit a problem, poll to completion, fetch the solution, check
it. The problem is built in code rather than loaded from a fixture file so it
does not depend on the routing datasets, and the assertions avoid pinning a
particular route ordering -- only that the solve succeeded and left no order
unserved.

GRPC_ROUTING_SETTINGS_MAPPER_TEST round-trips routing::solver_settings_t
through the proto. Six of its eight accessors previously had no test at all.
It covers the presence semantics an end-to-end solve cannot see: an unset
time_limit must not be serialized, since the solver derives its default from
absence, while an explicit zero must survive.

Both were checked by mutation. Removing the time_limit presence guard leaves
SolveVRP passing -- it sets an explicit limit, so it never exercises that
path -- while the mapper test fails. The end-to-end test is the right primary
but is not a superset of the mapper test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
…t' into feat/split-routing-lp-libs

# Conflicts:
#	cpp/CMakeLists.txt
#	cpp/src/routing/CMakeLists.txt
…-libs

# Conflicts:
#	cpp/src/routing/CMakeLists.txt
The comment said the direct solve_lp/mip_remote calls make libcuopt_grpc.so a
real DT_NEEDED of cuopt_cli. That stopped being true once solve_remote.cpp
moved into cuopt_client: the binary links cuopt_mathopt and cuopt_client, and
carries no DT_NEEDED on libcuopt_grpc.so at all.

The reasoning still holds, for a different reason. Calling the remote entry
points directly resolves them at link time from cuopt_client rather than
relying on ensure_remote_solvers_loaded() to dlopen libcuopt_grpc.so, which is
the path solve_lp/solve_mip take through the registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Two changes to the component boundaries, both found by measuring the built
libraries rather than reading the CMake.

cuopt_grpc no longer earns a component. After #1884 moved the routing arm into
cuopt_client, and solve_remote.cpp moved there too, the library was 18 KB
holding one source file: an ELF constructor calling register_remote_solvers.
It exported no cuOpt symbols at all -- its 11 dynamic symbols were incidental
raft and libstdc++ template instantiations leaked from headers. As a shipped
package it would have been a wheel and a conda output containing a constructor
and nothing callable.

grpc_registration.cpp now builds into cuopt_mathopt, which owns the registry it
fills. It still cannot build into cuopt_client: that would leave the client
library with an undefined register_remote_solvers and cost it the standalone
property. With the constructor in the same library as the registry, the dlopen
in ensure_remote_solvers_loaded() became dead code and is gone, along with the
dlfcn include; the function stays as a documented no-op because two call sites
read better for it.

Separately, cuopt_routing referenced 8 symbols from cuopt_client with no
DT_NEEDED on it -- the assignment_t and solver_settings_t accessors that #1884
moved. It resolved only because something else had pulled cuopt_client in. A
routing-only install, which is the point of #1635, would have failed at load.
cuopt_routing now links cuopt_client explicitly.

cuopt_grpc_server had been reaching cuopt_routing transitively through
cuopt_grpc, so it now links both engines directly. It is the one artifact that
solves both VRP and LP/MIP, so the dependency belongs in the open.

Every cross-library reference now has a matching DT_NEEDED, and cuopt_client
remains a leaf: no CUDA, no rmm, no raft, no undefined cuopt:: symbols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv ramakrishnap-nv changed the title build(cmake): split libcuopt into cuopt_client / cuopt_mathopt / cuopt_routing / cuopt_grpc component libs build(cmake): split libcuopt into cuopt_client / cuopt_mathopt / cuopt_routing component libs Sep 11, 2026
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

Addresses review: the atomic function pointers, the ELF-constructor
registration and the dlopen all existed to break a dependency cycle that no
longer exists.

When the registry was written, solve_lp_remote and solve_mip_remote lived in
the gRPC component, which depended on the math-opt library, so math-opt could
not call them. They now live in cuopt_client, which is a leaf, and cuopt_mathopt
already links it. The dependency is one-way, so the indirection buys nothing.

Both call sites collapse from a load-acquire on an atomic, a null check and an
indirect call to a direct call. Deleted: remote_solve_registry.{hpp,cpp},
grpc_registration.cpp, both std::atomic function pointers, the ready flag with
its release/acquire pairing, register_remote_solvers and
ensure_remote_solvers_loaded.

The one thing the registry did handle was a build without gRPC, where the
remote entry points do not exist and the null pointer degraded to a runtime
error. That is now a compile-time guard on CUOPT_ENABLE_GRPC, which is already
defined globally, and the same error text is kept for the no-gRPC case.

This also removes solve_lp_remote_fn_t and solve_mip_remote_fn_t, which a
reviewer had asked to unify into one type -- there is now no type to unify.

Remote execution verified end to end: CpuOnlyWithServerTest.lp_solve and
.mip_solve pass against a forked server, exercising the new direct dispatch
through the C API. log_callback_remote still fails, unchanged, as #1878.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

…-libs

# Conflicts:
#	cpp/src/io/mps_parser_internal.hpp
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

Every conda-python-tests job failed to import the Cython extensions:

  ImportError: libcuopt_client.so: undefined symbol:
    _ZN3rmm10_RMM_26_1016cuda_stream_viewC1EP11CUstream_st

cuopt_client_objs already lists rmm::rmm, but an OBJECT library does not
propagate its link interface to a target built from $<TARGET_OBJECTS:...>, so
libcuopt_client.so carried two undefined rmm symbols and no DT_NEEDED to
resolve them. That went unnoticed because libcuopt.so was a real ELF linking
rmm, so anything loading the client had rmm present already. Once libcuopt.so
became a linker script, a Cython extension loading the client directly had
nothing to resolve against.

The two symbols are cuda_stream_view's constructor, emitted by the static
initializers rmm's header creates in every including translation unit for
globals the client never names, and device_buffer's destructor, instantiated
through the std::variant in linear_programming_ret_t whose GPU alternative the
remote client never constructs. Neither is load-bearing, and #1890 tracks
removing the need; this restores correctness now.

librmm does not reintroduce CUDA: 1.5 MB, no CUDA in its own NEEDED, and its
only CUDA-ish dependency is cuda-version, a 21.6 KB metapackage with no files.
A GPU-free client install still works.

The check that missed this counted undefined cuopt:: symbols and NEEDED
entries, never undefined rmm:: symbols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

CpuOnlyWithServerTest.log_callback_remote failed on every conda-cpp-tests
config: the remote solve succeeded but no streamed log lines reached the
callback.

The components are built with hidden visibility, so each got its own copy of
the inline thread_local holding the registration. cuOptSetLogCallback lives in
cuopt_c.cpp, which builds into cuopt_mathopt, while the remote-solve log
forwarding reads the registration from solve_remote.cpp in cuopt_client. Two
copies, so the remote path saw no callback and silently delivered nothing.

  libcuopt_client    t_log_callback exported (client uses default visibility)
  libcuopt_mathopt   own hidden copy
  libcuopt_routing   own hidden copy

Marking it CUOPT_EXPORT gives every component default visibility for that
symbol, so the dynamic linker collapses the copies into one. Registration stays
scoped per thread, which is the property that keeps concurrent solves from
capturing each other's callback; sharing across libraries does not weaken it.

The header comment said the per-library copy was intended. That was true of the
per-thread scoping, not of the per-library split, which was an accident of
hidden visibility -- so the comment is rewritten to say why the export is
load-bearing.

This is a split-only bug: it passes on a monolithic build, which is why it
survived until libcuopt.so became a linker script.

Verified: log_callback_remote now passes, and the full C++ suite is clean when
run serially. Under -j2 five binaries fail with 22 RMM out-of-memory hits from
GPU contention on one card; all four affected binaries pass individually.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

SolveVRP timed out with the job still QUEUED on one conda-cpp-tests config.
The cause is placement, not the test body.

DefaultServerTests shares one server across the suite, and gtest runs in
declaration order, so the test sat immediately after CancelRunningJob:

  22  CancelRunningJob                 cancels a running job, kills a worker
  23  SolveVRP                         stuck in QUEUED, 30s timeout
  24  DeleteQueuedJobPreventsRun       failed
  25  DeleteRunningJobCancelsWorker    failed

Everything from the worker kill onward failed. That is the mechanism already
recorded in #1716: a job submitted after a cancel test kills the worker can sit
in QUEUED indefinitely. The two Delete tests are the known #1814 pair and fail
there regardless; SolveVRP became a new casualty by being placed among them.

It is a test of the routing mappers, not of recovering from a killed worker, so
it now sits with the other solve tests after SolveMIPBlocking, with a comment
recording why it belongs there.

The underlying server issue is untouched and still tracked in #1716 and #1814.
Whole suite now passes locally, 58/58.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants