Skip to content

libcuopt_client links librmm for two symbols it never uses #1890

Description

@ramakrishnap-nv

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 build(cmake): split libcuopt into cuopt_client / cuopt_mathopt / cuopt_routing component libs #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.

Activity

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

Metadata

Metadata

Assignees

Labels

awaiting responseThis expects a response from maintainer or contributor depending on who requested in last comment.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions