NVIDIA / NVIDIA/cuopt

libcuopt_client links librmm for two symbols it never uses

Open
#1,890 0 comments 0 reactions 1 assignee View on GitHub

@tmckayus is already working on this.

Since Sep 11, 2026.

awaiting response
Dominant language
Cuda
Stars
1k
Forks
233
Avg merge
4d 4h
Merged PRs (30d)
95

Description

Description

libcuopt_client.so links librmm for two symbols it never meaningfully uses:

rmm::cuda_stream_view::cuda_stream_view(CUstream_st*)
rmm::device_buffer::~device_buffer()

Neither comes from the client doing GPU work. Both come from the shape of types it is exposed to. The dependency is cheap today — librmm.so is 1.5 MB, carries no CUDA in its NEEDED, and its only CUDA-ish dependency is cuda-version, a 21.6 KB metapackage with zero files — so a GPU-free install still works. But the client is meant to be a leaf, and neither symbol is load-bearing.

Found while splitting libcuopt.so into components (#1622). Before that split the symbols resolved by accident: libcuopt.so was a real ELF that linked rmm, so anything loading the client had rmm already present. Once it became a linker script, a Cython extension loading libcuopt_client.so directly had nothing to resolve against, and every conda-python-tests job failed with:

ImportError: libcuopt_client.so: undefined symbol:
  _ZN3rmm10_RMM_26_1016cuda_stream_viewC1EP11CUstream_st

That is fixed by linking rmm::rmm into the cuopt_client target. Note cuopt_client_objs already listed it, but an OBJECT library does not propagate its link interface to a target built from $<TARGET_OBJECTS:...>, so the shared library never got it. This issue is about removing the need, not the link.

Symbol 1: cuda_stream_view constructor — pure waste

rmm/cuda_stream_view.hpp declares two namespace-scope globals:

static const cuda_stream_view cuda_stream_legacy{...};      // :132
static const cuda_stream_view cuda_stream_per_thread{...};  // :139

Being static at namespace scope, every translation unit including that header gets its own copy and its own static initializer. The reference shows up in .text.startup._GLOBAL__sub_I_<tu>.cpp, not in any code path — grpc_client_env.cpp, which only reads environment variables, carries one.

No client source ever names either global. This is emitted in 19 of the client's objects for something never used. Worth fixing on its own, independently of symbol 2.

Symbol 2: device_buffer destructor — forced by a variant

cython_types.hpp:

using gpu_buffer = std::unique_ptr<rmm::device_buffer>;

struct linear_programming_ret_t {
  struct gpu_solutions_t { gpu_buffer primal_solution_; /* 11 of these */ };
  struct cpu_solutions_t { /* std::vector<double> equivalents */ };
  std::variant<gpu_solutions_t, cpu_solutions_t> solutions_;
};

cython_grpc_client.cpp:222 assigns one of these. A std::variant destructor must handle every alternative, so instantiating it pulls in unique_ptr<rmm::device_buffer>'s destructor — even though the remote client only ever produces the CPU alternative. It pays for a branch it cannot take.

What will not work

Worth recording, because two obvious fixes make things worse:

  • Moving gpu_solutions_t's destructor out-of-line into a CUDA translation unit. The client would then need that symbol from cuopt_mathopt, creating a client -> mathopt edge and destroying the leaf property that lets the client ship without CUDA at all. Same objection applies to an opaque-handle deleter defined in mathopt.
  • Pimpl on the settings types. This was the first theory, and it is wrong: the dominant cost is a static initializer that fires on include, so hiding members behind a pointer does not remove it. (A shared_ptr pimpl was also tried earlier in this work and reverted for breaking pdlp_warm_start_data_t's deep-copy semantics.)

What would work

  • Symbol 1: keep rmm/cuda_stream_view.hpp out of client translation units, or include it through a path that does not instantiate those globals. Cheap and self-contained.
  • Symbol 2: give the client's return path a CPU-only type rather than sharing linear_programming_ret_t with its GPU alternative. This is the larger piece — it touches the Cython return types and therefore the Python binding layer, which is why it is not folded into #1622.

Acceptance

  • nm -D --undefined-only libcuopt_client.so | c++filt | grep -E 'rmm::|raft::|cuda' returns nothing
  • cuopt_client no longer links rmm::rmm, and librmm.so is absent from its DT_NEEDED
  • the client still has no DT_NEEDED on any other cuOpt library

Note the check that missed this originally counted undefined cuopt:: symbols and NEEDED entries only. Any guard added for this should assert on undefined rmm::/raft::/cuda symbols too.

Related: #1622, #1804, #1872, #1635.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.