Skip to content

build(cmake): split libcuopt into cuopt_base / cuopt_routing / cuopt_lp component libs - #1622

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

build(cmake): split libcuopt into cuopt_base / cuopt_routing / cuopt_lp component libs#1622
ramakrishnap-nv wants to merge 31 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 four component SHARED libraries — cuopt_base, cuopt_routing, cuopt_mathematical_optimization, cuopt_grpc — 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_base.so libcuopt_routing.so libcuopt_mathematical_optimization.so libcuopt_grpc.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 and the consumer ends up with a direct DT_NEEDED on each component. cuopt::cuopt is now an INTERFACE target, so find_package(cuopt) and the Cython modules are unchanged.

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::base, cuopt::routing, cuopt::mathematical_optimization and cuopt::grpc.

Components

Sizes are stripped. "Runtime" is the measured DT_NEEDED; "CMake PUBLIC" is what propagates to consumers at link time. They differ because --as-needed drops libraries a component does not call.

Component Size Runtime deps (beyond rmm / rapids_logger) CMake PUBLIC deps
cuopt_base 1.17 MB rmm, rapids_logger, CCCL, raft, CUDA::cublas, CUDA::cusparse
cuopt_routing 37 MB cublas cuopt_base
cuopt_mathematical_optimization 59 MB cublas, cusparse, nccl, cudss, TBB cuopt_base, cudss
cuopt_grpc 1.0 MB grpc, protobuf, abseil cuopt_mathematical_optimization, cuopt_routing

Neither engine has a DT_NEEDED on the other; only cuopt_grpc links both. cuopt_base exists because routing and mathematical optimization both resolve default_logger() and seed_generator::seed_ from it, and those are process-global singletons that must not be duplicated.

Remote solve

cuopt_mathematical_optimization no longer hard-links gRPC. It holds nullable callback slots that libcuopt_grpc.so's ELF constructor fills in via register_remote_solvers(), and loads that component on demand. A g_remote_solvers_ready flag is published with release ordering after both callbacks are stored, so a reader that observes it sees both.

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.

Testing

Full build plus ctest (131/131) on a local RTX 8000. Verified separately: bare -lcuopt links without --allow-shlib-undefined and the resulting binary runs; find_package(cuopt) resolves all five targets; load.py loads all four components and the C API resolves.

Next steps (#1635)

  1. Relink the Cython extension modulespython/cuopt/cuopt/{routing,linear_programming/solver,grpc/linear_programming,distance_engine}/CMakeLists.txt all 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 cuopt_grpc's LP and routing proto mappers so a routing-only gRPC client is possible.
  4. Split the wheels and conda recipeslibcuopt-base/routing/mathematical-optimization/grpc plus a libcuopt metapackage for backward compatibility.

🤖 Generated with Claude Code

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.
Title check ✅ Passed The title clearly identifies the main CMake change: splitting libcuopt into component libraries, although it omits some components and names the LP component imprecisely.
Description check ✅ Passed The description directly explains the component-library split, packaging impact, remote-solve design, testing, and follow-up work.
✨ 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

✅ 2 passed · 2 skipped · 11 cancelled / not completed

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 ramakrishnap-nv removed the do not merge Do not merge if this flag is set label Aug 5, 2026
@ramakrishnap-nv
ramakrishnap-nv marked this pull request as draft August 5, 2026 19:42
…y races

- Add umbrella anchors for cuopt_base and cuopt_routing. libcuopt.so listed
  only libcuopt_lp.so and libcuopt_grpc.so in DT_NEEDED; with --as-needed the
  base and routing components were dropped, so routing vanished entirely under
  SKIP_GRPC_BUILD.
- Export components as cuopt::base/routing/lp/grpc via EXPORT_NAME, matching
  the names advertised in the package doc string.
- Make the remote-solve registry slots std::atomic. The gRPC ELF constructor
  publishes them during a lazy dlopen while other threads read them.
- Move the lazy dlopen into ensure_remote_solvers_loaded(), shared by the LP
  and MIP dispatchers.
- Validate the CPU-problem downcast in cuopt_cli instead of static_cast.
- Add cuopt_routing and cuopt_grpc to the pip wheel RPATH list.
- Fall back to "unknown" for GIT_COMMIT_HASH in non-git source trees.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test 91f5b84

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 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 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
cpp/src/pdlp/remote_solve_registry.cpp (1)

19-24: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add ${CMAKE_DL_LIBS} to cuopt_lp
remote_solve_registry.cpp uses dlopen, but cuopt_lp has no explicit dynamic-loader dependency. Add ${CMAKE_DL_LIBS} for portability. Current Ubuntu LTS releases resolve dlopen from libc, so this is not a current Ubuntu LTS link failure.

🤖 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/src/pdlp/remote_solve_registry.cpp` around lines 19 - 24, Add
${CMAKE_DL_LIBS} to the cuopt_lp target's link dependencies so the dlopen call
in ensure_remote_solvers_loaded has an explicit, portable dynamic-loader
dependency. Preserve the existing target configuration and avoid changing the
remote solver loading logic.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@cpp/CMakeLists.txt`:
- Line 1035: Update both GLOBAL_TARGETS entries in cpp/CMakeLists.txt at lines
1035 and 1044 to use ${CUOPT_COMPONENT_TARGETS} instead of
${CUOPT_COMPONENT_EXPORT_NAMES}, while preserving the existing cuopt target.

In `@cpp/src/pdlp/remote_solve_registry.cpp`:
- Around line 15-16: Update the callback publication order in the remote
registration routine so g_solve_mip_remote_fn is stored before
g_solve_lp_remote_fn, since the LP slot is the readiness sentinel. Add a
concurrent registration and MIP-dispatch unit test that verifies MIP dispatch
cannot observe a ready registry with a null MIP callback.

In `@python/libcuopt/CMakeLists.txt`:
- Around line 98-102: Update the target property assignment in the foreach block
for cuopt-related targets so the CMake set_property arguments place APPEND
before PROPERTY, preventing APPEND from being interpreted as an INSTALL_RPATH
entry. Also update the relevant package checks to reject relative runtime paths.

---

Nitpick comments:
In `@cpp/src/pdlp/remote_solve_registry.cpp`:
- Around line 19-24: Add ${CMAKE_DL_LIBS} to the cuopt_lp target's link
dependencies so the dlopen call in ensure_remote_solvers_loaded has an explicit,
portable dynamic-loader dependency. Preserve the existing target configuration
and avoid changing the remote solver loading logic.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7351d224-a16d-48f8-838b-f7f085862934

📥 Commits

Reviewing files that changed from the base of the PR and between 80c1c34 and 91f5b84.

📒 Files selected for processing (7)
  • cpp/CMakeLists.txt
  • cpp/cuopt_cli.cpp
  • cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp
  • cpp/src/mip_heuristics/solve.cu
  • cpp/src/pdlp/remote_solve_registry.cpp
  • cpp/src/pdlp/solve.cu
  • python/libcuopt/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (3)
  • cpp/src/pdlp/solve.cu
  • cpp/src/mip_heuristics/solve.cu
  • cpp/cuopt_cli.cpp

Comment thread cpp/CMakeLists.txt
Comment thread cpp/src/pdlp/remote_solve_registry.cpp Outdated
Comment thread python/libcuopt/CMakeLists.txt Outdated
ramakrishnap-nv and others added 2 commits August 6, 2026 10:30
register_remote_solvers() stored the LP callback before the MIP callback while
ensure_remote_solvers_loaded() used the LP slot as its readiness sentinel. A
concurrent MIP solve could observe the LP slot set, skip the lazy dlopen, then
read a still-null MIP slot and fail with a spurious "gRPC component not loaded"
error. Publish a separate ready flag after both callbacks instead, so readiness
does not depend on callback count or store order.

Also report dlopen failures via dlerror() rather than discarding them, and link
cuopt_lp against ${CMAKE_DL_LIBS} since it calls dlopen directly.

Use the documented APPEND-before-PROPERTY form in the pip RPATH loop.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test a5000c3

@ramakrishnap-nv
ramakrishnap-nv marked this pull request as ready for review August 6, 2026 15:31
* exceptions across the component boundary. Only the `<int, double>`
* instantiation is supported.
*/
using solve_lp_remote_fn_t = std::unique_ptr<lp_solution_interface_t<int, double>> (*)(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should unify these and just have one solve_remote_fn_t

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@tmckayus for viz!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I tried this and backed it out — I don't think there's a version that comes out ahead. The two callbacks differ in both positions that matter, not just cosmetically.

Settings have no common base. pdlp_solver_settings_t (pdlp/solver_settings.hpp:113) and mip_solver_settings_t (mip/solver_settings.hpp:65) are unrelated classes. The combined solver_settings_t can't stand in either: it's non-copyable and non-movable (all four special members deleted), and neither dispatch site has one — solve_lp/solve_mip only ever receive the specific settings object.

Return types do share a base, but unifying on it costs more than it saves. Both solve_lp and solve_mip return the concrete interface and today pass the callback result straight through:

return remote_fn(*cpu_prob, settings);   // solve.cu:2699, mip_heuristics/solve.cu:921

A base-typed callback forces a downcast back at both sites. And since function-pointer types are invariant, &solve_lp_remote<int, double> wouldn't bind to a base-returning typedef without also changing the public solve_remote.hpp signatures — which pushes the same downcast into cuopt_cli. That's the exact conversion pattern flagged earlier in this review.

That leaves two shapes, neither good:

  • Type-erased settings (void const* + tag, or a variant) — trades two precisely-typed callbacks for one untyped one.
  • An alias template — gives one name but still two distinct types, turns 4 lines into 9, and makes you resolve a template to see what the signature actually is. I wrote it, read it back, and it was worse.

The only genuine unification is a shared base for the two settings classes. That's a public-header change plus a virtual hierarchy on settings, and the payoff is deleting one duplicated parameter type from a typedef. I don't think that trade is worth it right now — but if a third remote solver shows up the balance changes, and it'd be worth revisiting then.

Leaving the two typedefs as-is unless you or @tmckayus feel strongly.

Comment thread ci/build_wheel_cuopt.sh Outdated
--exclude "libcuopt.so"
--exclude "libcuopt_base.so"
--exclude "libcuopt_routing.so"
--exclude "libcuopt_lp.so"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We might want to call this libcuopt_mathematical_optimization.so. We are trying to move away from lp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Isn't it a very long name for a library? Normally libraries have very short names and many libraries use abbreviations. Maybe we could do something like: libcuopt_mathopt.so ?

Comment thread cpp/src/CMakeLists.txt Outdated
# Aggregate per-domain source lists for the three component libraries
set(CUOPT_BASE_SRC_FILES
${UTIL_SRC_FILES}
${LINEAR_ALGEBRA_SRC_FILES}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Linear algebra files are only used inside the mathematical optimization (CUOPT_LP_SRC_FILES), I would put them directly in CUOPT_LP_SRC_FILES

Comment thread cpp/CMakeLists.txt Outdated
add_library(cuopt::routing ALIAS cuopt_routing)
endif()

# cuopt_lp: LP / MIP / numerical optimization engine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's call this cuopt_mathematical_optimization: LP / QP / SOCP / MIP

Comment thread cpp/CMakeLists.txt Outdated
# DT_NEEDED. The constructor in grpc_registration.cpp fires when
# libcuopt_grpc.so is loaded and wires up the remote-solve function pointers
# in libcuopt_lp.so, so that must happen whenever -lcuopt is used.
set(_UMBRELLA_SRC [=[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we have source code in the CMakeList.txt? Maybe pull this out into an umbrella.cpp or umbrella.cpp.in and just configure it as appropriate in the CMakeList.txt.

@chris-maes chris-maes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is concerning to me:

C API users must now link the component that defines the symbols, e.g. -lcuopt -lcuopt_lp for LP/MILP/QP.

It's quite annoying to have to link multiple libraries. Is there a way around this?

ramakrishnap-nv and others added 3 commits August 6, 2026 16:08
The component covers LP, QP, SOCP and MIP, so name it for what it is.
Renames the target, the shipped libcuopt_mathematical_optimization.so, the
cuopt::mathematical_optimization export, and the source-list variable.

Also moves the linear algebra sources out of cuopt_base and into the
mathematical optimization component: routing resolves only default_logger()
and seed_generator::seed_ from base and references no linear algebra symbols,
so they do not belong in the shared base. cuopt_base drops to 1.17 MB.

Extracts the umbrella translation unit out of CMakeLists.txt into
cmake/umbrella.cpp.in, configured with #cmakedefine rather than assembled
with string(APPEND). The C++ now lives in a real .cpp file that can be
formatted and read on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Bare -lcuopt stopped resolving once libcuopt.so became a thin umbrella: the
linker does not satisfy a consumer's undefined symbols through a dependency's
DT_NEEDED (--no-copy-dt-needed-entries, the default since binutils 2.22), so C
consumers had to name each component and the example Makefiles had to paper
over it with --allow-shlib-undefined.

Ship libcuopt.so as a GNU ld script naming the components instead. This is how
glibc ships libc.so. -lcuopt resolves to the whole set again and consumers get
a direct DT_NEEDED on each component, so the umbrella library and its
--as-needed anchor symbols are no longer needed and are removed. cuopt::cuopt
becomes an INTERFACE target, leaving CMake and Cython consumers unchanged.

libcuopt.so is no longer an ELF object, so it cannot be dlopen()ed: load.py
loads the components directly, and the wheel omits the script since nothing in
it links. Anything previously linked against libcuopt.so needs a rebuild.

Also fixes WRITE_FATBIN, which applied fatbin.ld to the umbrella. That target
held no device code after the split, so the section grouping had silently
become a no-op; it now applies to the components carrying the fatbins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Rewraps comments that exceeded the column limit after the
cuopt_mathematical_optimization rename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Comment thread cpp/CMakeLists.txt
add_library(cuopt::base ALIAS cuopt_base)

# cuopt_routing: VRP / routing engine (omitted when SKIP_ROUTING_BUILD=ON)
if(NOT SKIP_ROUTING_BUILD)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we intend to build with

./build.sh libcuopt_routing
./build.sh libcuopt_mathematical_optimization
./build.sh libcuopt_grpc

If so do we still need SKIP_ROUTING_BUILD etc?

"Remote execution requires CPU memory backend");
return solve_mip_remote(*cpu_prob, settings);
ensure_remote_solvers_loaded();
auto* remote_fn = g_solve_mip_remote_fn.load(std::memory_order_acquire);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think we should need an atomic for calling the grpc solver.
I believe this comes from a circular dependency of libgrpc on libmathematical_optimizaiton since libgrpc uses cpu_optimization_problem_t, mip_solver_settings_t, and solution types

The umbrella/CLI/Python layer should select local versus remote, with cuopt_grpc depending one-way on mathematical-optimization types. Can we remove the dlopen/atomic registration mechanism entirely?


#include <dlfcn.h>

namespace cuopt::mathematical_optimization {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since libcuopt.so already links both mathematical optimization and gRPC, could it own the local-versus-remote dispatch? Then the mathematical-optimization library remains local-only, the dependency stays one-way, and we can remove dlopen, constructor registration, and the atomic function pointers.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed on the substance — the registry is working around a layering inversion, and cuopt_mathematical_optimization should not know about remote execution at all. Tracked as #1692.

Two notes on the specifics.

The umbrella can no longer own the dispatch. As of b3b8702 libcuopt.so is a GNU ld script rather than a shared library:

INPUT(libcuopt_base.so libcuopt_routing.so libcuopt_mathematical_optimization.so libcuopt_grpc.so)

That was to fix the -lcuopt breakage @chris-maes raised: the linker does not resolve a consumer's undefined symbols through a dependency's DT_NEEDED (--no-copy-dt-needed-entries, default since binutils 2.22), so a thin umbrella forced C consumers to name every component. The script restores plain -lcuopt, but it means there is no umbrella object left to host dispatch.

The atomic is a consequence, not a choice. With the lazy dlopen, libcuopt_grpc.so's ELF constructor publishes the pointers on whichever thread triggers the load while other threads may be reading them — a real race (flagged earlier in this review, and the reason the readiness flag exists). Remove the lazy-load design and the atomics go with it; they are not independently justified.

On where dispatch should move: #1597 already does exactly what you are describing, for routing. No CUOPT_REMOTE_HOST, no is_remote_execution_enabled, no registry, no dlopen — an explicit RoutingClient in cuopt_grpc, with cpu_routing_problem_t as a plain type in cuopt_routing. LP/MIP's env-var dispatch is the outlier. #1692 proposes LPClient / MIPClient mirroring it.

I would rather not do that conversion here. It is a user-visible API change for LP/MIP, it means relocating cuopt_c.cpp and cython_solve.cu (the only two remaining dispatch sites outside the CLI), and #1597 already owns the gRPC client layer and has settled the shape. Doing it there avoids duplicating that design and keeps this PR to the build split.


On the SKIP_ROUTING_BUILD question: yes, still needed. There are no per-component build.sh targets — VALIDARGS is libcuopt / cuopt_grpc_server / cuopt / cuopt_server / cuopt_sh_client / docs / deb. The two do different jobs: cmake --build . --target cuopt_routing controls an incremental build, whereas SKIP_ROUTING_BUILD omits routing from the install set, the export set, the linker script's INPUT(...) list and load.py's expectations. A per-target build cannot express "ship a cuOpt without routing".

Integrates #1625 (symbol visibility controls and exports) with the component
split. Beyond the three textual conflicts, the two changes interact in ways
git could not see:

- #1625 applies hidden visibility to cuopt_objs, which in main is libcuopt.so.
  On this branch cuopt_objs only backs cuopt_static for tests, so the shipped
  component libraries would have kept default visibility. The presets now live
  in cuopt_configure_component().

- Hidden visibility then broke the inter-component ABI. Symbols that were
  internal to a single libcuopt.so now cross library boundaries, and shared
  libraries link fine with undefined symbols, so this only surfaced at runtime
  as symbol lookup errors in 10 tests. Eleven symbols across six headers make
  up that surface; they are annotated CUOPT_EXPORT.

- check_symbols.sh ran against libcuopt.so, which is now a linker script, and
  asserted the C API is exported from every library. Only mathematical
  optimization provides the C API, so the recipe checks the components and
  passes --no-public-api-check where the C API does not belong. The
  forbidden-symbol checks still run everywhere.

Build clean, ctest 125/125, check_symbols passes on all four components.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
One conflict in the conda recipe: main renamed date_string to
datetime_string (#1661) in the same block where this branch replaced the
prefix_detection entry for libcuopt.so with the component libraries. Both
changes kept.

Build clean, ctest 125/125, check_symbols passes on all four components.

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

Copy link
Copy Markdown
Collaborator Author

Fixed in b3b8702-lcuopt alone works again, no component libraries on the link line and no --allow-shlib-undefined.

libcuopt.so is now a GNU ld script rather than a shared library:

INPUT(libcuopt_base.so libcuopt_routing.so libcuopt_mathematical_optimization.so libcuopt_grpc.so)

This is the mechanism glibc uses — /usr/lib/x86_64-linux-gnu/libc.so is the same kind of file. 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 could never have satisfied -lcuopt no matter what it linked.

Verified end to end: gcc app.c -lcuopt links, the binary runs, and it ends up with a direct DT_NEEDED on each component. The example Makefiles are back to plain -lcuopt and --allow-shlib-undefined is gone. find_package(cuopt) is unchanged and still exposes cuopt::cuopt (now an INTERFACE target) plus cuopt::base, cuopt::routing, cuopt::mathematical_optimization and cuopt::grpc.

Two consequences worth flagging before this merges:

  • dlopen("libcuopt.so") no longer works, since the file is not ELF. Load the components instead — load.py does this now.
  • Anything previously linked against libcuopt.so needs a rebuild, because its DT_NEEDED names a file that is no longer an object. Given we break ABI each release this is a release note rather than an incident, but it should be called out.

This also removed the umbrella library and its --as-needed anchor symbols entirely, so the machinery that was there purely to stop the linker discarding components is gone.


Your other comments are addressed as well:

  • cuopt_lpcuopt_mathematical_optimization throughout, including the shipped libcuopt_mathematical_optimization.so and the cuopt::mathematical_optimization export (1c4cdbd).
  • Linear algebra moved out of cuopt_base into the mathematical optimization component. Confirmed routing resolves only default_logger() and seed_generator::seed_ from base and references no linear algebra symbols; cuopt_base drops to 1.17 MB.
  • The umbrella source is out of CMakeLists.txt. It briefly moved to cmake/umbrella.cpp.in, then the linker script removed the need for it altogether.

On "why do we need cuopt_base" — routing and mathematical optimization both resolve default_logger() and seed_generator::seed_ from it, and those are process-global singletons. Header-only would give each component its own copy, so setting the log level or seed in one would not affect the other. Folding base into mathematical optimization would reach three libraries, but routing would then depend on the 59 MB solver plus cusparse/nccl/cudss, which defeats the purpose of the split. It is ~1.2 MB of shared state that keeps the two engines independent.

ramakrishnap-nv and others added 3 commits August 7, 2026 14:17
'1' is shell-redirection output and small_mip.mps is a local test problem;
neither belongs in the repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
cuopt became an INTERFACE target when libcuopt.so turned into a linker script,
and PRIVATE/PUBLIC are not valid on a target that compiles nothing, so the
libcuopt wheel build failed to configure:

  CMake Error at CMakeLists.txt:65 (target_link_libraries):
    INTERFACE library can only be used with the INTERFACE keyword of
    target_link_libraries

The call was redundant anyway. argparse is used only by cuopt_cli.cpp and
grpc_server_main.cpp, and cpp/CMakeLists.txt already links argparse::argparse
into both cuopt_cli and cuopt_grpc_server.

Verified by running ci/build_wheel_libcuopt.sh in the CI wheel image: the wheel
builds, auditwheel repair succeeds, pydistcheck reports no errors and twine
passes. The wheel contains the four component libraries and not the linker
script. python/libcuopt is only configured by the wheel build, which is why a
local cpp/build could not catch this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
The wheel installs 2020 C/C++ headers including cuopt_c.h, and the C API docs
tell users to pip install libcuopt-cuXX then locate libcuopt.so and link
-lcuopt. Excluding the linker script left that path with headers but nothing to
link against.

The exclusion was guarding against auditwheel choking on a non-ELF .so, which
it does not do: auditwheel's elf_file_filter parses each candidate and skips
anything raising ELFError, so the script is copied through untouched. Its
--exclude option skips SONAMEs from being grafted, not files from being
processed, so no exception was needed in the first place.

Verified with ci/build_wheel_libcuopt.sh in the CI wheel image: auditwheel
repair, pydistcheck and twine all pass, and the repaired wheel contains the
linker script alongside the four component libraries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
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.

5 participants