Background
LP/MIP remote execution is triggered implicitly: solve_lp / solve_mip check is_remote_execution_enabled() (set by CUOPT_REMOTE_HOST / CUOPT_REMOTE_PORT) and reroute. Because that check lives inside cuopt_mathematical_optimization, the engine needs to call into cuopt_grpc — the reverse of the dependency direction, since cuopt_grpc already links the engine.
The workaround is a function-pointer registry: cuopt_mathematical_optimization holds nullable slots that libcuopt_grpc.so's ELF constructor fills via register_remote_solvers(), reached through a lazy dlopen. That lazy load is also why the slots have to be atomic — the constructor publishes them on whichever thread triggers the load while others may be reading.
Raised by @hlinsen in #1622:
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?
The pattern already exists
#1597 (routing over gRPC) does this the clean way. Zero uses of CUOPT_REMOTE_HOST, is_remote_execution_enabled or get_memory_backend_type; no registry, no dlopen, no constructor registration. The user asks for remote explicitly:
client = RoutingClient("gpu-host:50051")
client.solve(data_model, settings)
cpu_routing_problem_t is a plain data type in cuopt_routing; the mapper, client and server live in cuopt_grpc. The dependency stays one-way and the routing engine stays local-only.
LP/MIP is the outlier, not the pattern.
Proposal
Add LPClient / MIPClient mirroring RoutingClient, then delete the registry:
cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp
cpp/src/pdlp/remote_solve_registry.cpp
cpp/src/grpc/client/grpc_registration.cpp
- the remote branches in
cpp/src/pdlp/solve.cu, cpp/src/mip_heuristics/solve.cu, cpp/src/pdlp/utilities/cython_solve.cu and cpp/cuopt_cli.cpp
cuopt_mathematical_optimization becomes local-only and every component dependency is one-way. The dlopen, the atomics and the ELF constructor all go with it.
Open question
What happens to CUOPT_REMOTE_HOST / CUOPT_REMOTE_PORT. Keeping them as a deprecated compatibility path preserves existing users' setups; removing them is cleaner but is a user-visible break. This is a product decision, not a build one.
Sequencing
Depends on #1622 (component split) and #1597 (routing over gRPC). Best done in or right after #1597, which already owns the gRPC client layer and establishes the pattern — rather than in #1622, which would mean duplicating that design work and adding a user-visible API change to a build-focused PR.
Background
LP/MIP remote execution is triggered implicitly:
solve_lp/solve_mipcheckis_remote_execution_enabled()(set byCUOPT_REMOTE_HOST/CUOPT_REMOTE_PORT) and reroute. Because that check lives insidecuopt_mathematical_optimization, the engine needs to call intocuopt_grpc— the reverse of the dependency direction, sincecuopt_grpcalready links the engine.The workaround is a function-pointer registry:
cuopt_mathematical_optimizationholds nullable slots thatlibcuopt_grpc.so's ELF constructor fills viaregister_remote_solvers(), reached through a lazydlopen. That lazy load is also why the slots have to be atomic — the constructor publishes them on whichever thread triggers the load while others may be reading.Raised by @hlinsen in #1622:
The pattern already exists
#1597 (routing over gRPC) does this the clean way. Zero uses of
CUOPT_REMOTE_HOST,is_remote_execution_enabledorget_memory_backend_type; no registry, nodlopen, no constructor registration. The user asks for remote explicitly:cpu_routing_problem_tis a plain data type incuopt_routing; the mapper, client and server live incuopt_grpc. The dependency stays one-way and the routing engine stays local-only.LP/MIP is the outlier, not the pattern.
Proposal
Add
LPClient/MIPClientmirroringRoutingClient, then delete the registry:cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hppcpp/src/pdlp/remote_solve_registry.cppcpp/src/grpc/client/grpc_registration.cppcpp/src/pdlp/solve.cu,cpp/src/mip_heuristics/solve.cu,cpp/src/pdlp/utilities/cython_solve.cuandcpp/cuopt_cli.cppcuopt_mathematical_optimizationbecomes local-only and every component dependency is one-way. Thedlopen, the atomics and the ELF constructor all go with it.Open question
What happens to
CUOPT_REMOTE_HOST/CUOPT_REMOTE_PORT. Keeping them as a deprecated compatibility path preserves existing users' setups; removing them is cleaner but is a user-visible break. This is a product decision, not a build one.Sequencing
Depends on #1622 (component split) and #1597 (routing over gRPC). Best done in or right after #1597, which already owns the gRPC client layer and establishes the pattern — rather than in #1622, which would mean duplicating that design work and adding a user-visible API change to a build-focused PR.