libcuopt_client links librmm for two symbols it never uses
@tmckayus is already working on this.
Since Sep 11, 2026.
- 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 fromcuopt_mathopt, creating aclient -> mathoptedge 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_ptrpimpl was also tried earlier in this work and reverted for breakingpdlp_warm_start_data_t's deep-copy semantics.)
What would work
- Symbol 1: keep
rmm/cuda_stream_view.hppout 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_twith 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 nothingcuopt_clientno longer linksrmm::rmm, andlibrmm.sois absent from itsDT_NEEDED- the client still has no
DT_NEEDEDon 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.