diff --git a/docs/cuopt/source/_static/large-rubric.css b/docs/cuopt/source/_static/large-rubric.css new file mode 100644 index 0000000000..3314a2e26f --- /dev/null +++ b/docs/cuopt/source/_static/large-rubric.css @@ -0,0 +1,7 @@ +/* Rubric styled closer to an H2/H3 for in-page subheads that must stay out of the TOC. */ +p.rubric.large-rubric { + font-size: 1.4em; + font-weight: 600; + margin-top: 1.75rem; + margin-bottom: 0.75rem; +} diff --git a/docs/cuopt/source/conf.py b/docs/cuopt/source/conf.py index b4749d0167..d078382e77 100644 --- a/docs/cuopt/source/conf.py +++ b/docs/cuopt/source/conf.py @@ -172,7 +172,11 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] -html_css_files = ["swagger-nvidia.css", "install-selector.css"] +html_css_files = [ + "swagger-nvidia.css", + "install-selector.css", + "large-rubric.css", +] html_js_files = ["cuopt-install-version.js", "install-selector.js"] html_extra_path = ["versions1.json"] diff --git a/docs/cuopt/source/cuopt-grpc/advanced.rst b/docs/cuopt/source/cuopt-grpc/advanced.rst index a092f026c3..7f9050ea10 100644 --- a/docs/cuopt/source/cuopt-grpc/advanced.rst +++ b/docs/cuopt/source/cuopt-grpc/advanced.rst @@ -66,10 +66,18 @@ These variables apply when the container **entrypoint** builds a ``cuopt_grpc_se The REST server path in the same image still uses ``CUOPT_SERVER_PORT`` for HTTP in other docs; that is separate from the gRPC defaults above. -Bundled Remote Client (Python, C API, ``cuopt_cli``) ----------------------------------------------------- +Integrated Remote Client (Python, C API, ``cuopt_cli``) +------------------------------------------------------- -Remote mode is active when **both** ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` are set. A **custom** gRPC client does not read these automatically; it must configure the channel and protos itself (see :doc:`api`). +These variables apply to **remote execution**: the client integrated into the +Python solver APIs, the C API (``cuOptSolve``), and ``cuopt_cli``. Remote mode +is active when **both** ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` are set. + +The :doc:`Python async gRPC client ` does **not** use +``CUOPT_REMOTE_HOST`` / ``CUOPT_REMOTE_PORT``; you pass host and port to +``Client(...)``. See *Python async gRPC client* below for variables that apply +to that client. A **custom** gRPC client must configure the channel itself +(see :doc:`api`). .. list-table:: :header-rows: 1 @@ -82,7 +90,7 @@ Remote mode is active when **both** ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_POR * - ``CUOPT_REMOTE_HOST`` - For remote - — - - Server hostname or IP + - GPU server hostname or IP * - ``CUOPT_REMOTE_PORT`` - For remote - — @@ -116,6 +124,48 @@ Remote mode is active when **both** ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_POR - ``0`` - Non-zero: extra gRPC client logging +Python Async gRPC Client (``cuopt.grpc``) +----------------------------------------- + +``Client(host, port, tls=...)`` takes the server address in code. It does +**not** read ``CUOPT_REMOTE_HOST`` or ``CUOPT_REMOTE_PORT``. + +When ``tls`` is omitted (``None``), the client honors the same ``CUOPT_TLS_*`` +variables as remote execution. Pass ``tls=False`` for plain TCP, or +``tls=TlsConfig(...)`` for explicit PEM paths (see :doc:`python-async-client`). + +.. list-table:: + :header-rows: 1 + :widths: 26 14 18 42 + + * - Variable + - Required + - Default + - Description + * - ``CUOPT_TLS_ENABLED`` + - No + - ``0`` + - Used when ``tls=None``; non-zero enables TLS + * - ``CUOPT_TLS_ROOT_CERT`` + - If TLS + - — + - PEM path to verify the **server** certificate + * - ``CUOPT_TLS_CLIENT_CERT`` + - mTLS + - — + - Client certificate PEM + * - ``CUOPT_TLS_CLIENT_KEY`` + - mTLS + - — + - Client private key PEM + * - ``CUOPT_GRPC_DEBUG`` + - No + - ``0`` + - Non-zero: extra gRPC client logging + +``CUOPT_CHUNK_SIZE`` and ``CUOPT_MAX_MESSAGE_BYTES`` also apply to this client +when set (same defaults as the integrated remote client). + Usage ===== @@ -259,14 +309,14 @@ Bypass the entrypoint: Client Environment (Examples) ------------------------------ -**Required** for remote (see *Bundled remote client* table for all variables): +**Remote execution** — required host/port (see *Integrated remote client* table): .. code-block:: bash export CUOPT_REMOTE_HOST= export CUOPT_REMOTE_PORT=5001 -**TLS** (optional): +**TLS** (optional; also used by the Python async gRPC client when ``tls=None``): .. code-block:: bash @@ -280,13 +330,44 @@ For mTLS, also: export CUOPT_TLS_CLIENT_CERT=client.crt export CUOPT_TLS_CLIENT_KEY=client.key +**Python async gRPC client** — pass host and port to ``Client(...)`` (not +``CUOPT_REMOTE_*``). With ``tls=None`` (default), the same ``CUOPT_TLS_*`` +variables above apply. For explicit PEM paths, use ``TlsConfig``: + +.. code-block:: python + + from cuopt.grpc.linear_programming import Client, TlsConfig + + # TLS: verify the server with a CA (or omit root_certs for the system trust store) + client = Client( + "server.example.com", + 5001, + tls=TlsConfig(root_certs="ca.crt"), + ) + + # mTLS: also present a client certificate + client = Client( + "server.example.com", + 5001, + tls=TlsConfig( + root_certs="ca.crt", + client_cert="client.crt", + client_key="client.key", + ), + ) + + # Plain TCP (ignore CUOPT_TLS_* even if set) + client = Client("localhost", 5001, tls=False) + +See :doc:`python-async-client` for the full job API. + Limitations and Scope ===================== -* **Problem types** — **LP**, **MILP**, and **QP** are supported on the gRPC remote path. **Routing** (VRP, TSP, PDP) is **not** supported yet; use the :doc:`REST self-hosted server <../cuopt-server/index>` for remote routing until a future release adds routing over ``CuOptRemoteService``. +* **Problem types** — **LP**, **MIP**, and **QP** are supported on the gRPC remote path. **Routing** (VRP, TSP, PDP) is **not** supported yet; use the :doc:`REST self-hosted server <../cuopt-server/index>` for remote routing until a future release adds routing over ``CuOptRemoteService``. * **Message size** — Large problems use chunking; very large models can still hit gRPC max message / timeout limits. Tune ``CUOPT_CHUNK_SIZE``, ``CUOPT_MAX_MESSAGE_BYTES``, server ``--max-message-mb``, and solver ``time_limit`` as needed. * **``CUOPT_GRPC_ARGS``** — Parsed on whitespace only; arguments containing spaces are awkward unless you invoke ``cuopt_grpc_server`` directly. -* **CRL / OCSP** — Not handled by the bundled gRPC TLS stack; use a private CA rotation strategy or a TLS-terminating proxy if you need revocation workflows. +* **CRL / OCSP** — Not handled by the integrated gRPC TLS stack; use a private CA rotation strategy or a TLS-terminating proxy if you need revocation workflows. Troubleshooting =============== @@ -310,5 +391,6 @@ Further Reading =============== * :doc:`quick-start` — Plain TCP quick path. -* :doc:`examples` — Links to Python, C, and CLI example sections (use with ``CUOPT_REMOTE_*`` on the client). +* :doc:`examples` — Links to Python, C, and CLI example sections (use with ``CUOPT_REMOTE_*`` on the client for remote execution). +* :doc:`python-async-client` — Explicit Python gRPC client. * :doc:`grpc-server-architecture` — Process model and job behavior (operator overview). diff --git a/docs/cuopt/source/cuopt-grpc/api.rst b/docs/cuopt/source/cuopt-grpc/api.rst index 70b91465ff..918108723c 100644 --- a/docs/cuopt/source/cuopt-grpc/api.rst +++ b/docs/cuopt/source/cuopt-grpc/api.rst @@ -11,7 +11,16 @@ The **CuOptRemoteService** gRPC API is defined in Protocol Buffers under the ``c * ``cpp/src/grpc/cuopt_remote_service.proto`` — service and job/chunk/log RPCs * ``cpp/src/grpc/cuopt_remote.proto`` — LP/MIP problem, settings, and result messages -Most users do **not** call these RPCs directly: the NVIDIA cuOpt **Python** API, **C API**, and **cuopt_cli** submit jobs using solver APIs plus :doc:`environment variables `. **Custom** clients call ``CuOptRemoteService`` over gRPC using these definitions. This page summarizes the service for custom integrators and debugging. +Most users do **not** call these RPCs directly: + +* **Remote execution** — Python, C (``cuOptSolve``), and ``cuopt_cli`` forward + solves when ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` are set + (:doc:`quick-start`, :doc:`advanced`). +* **Python async gRPC client** — ``cuopt.grpc.linear_programming.Client`` + (:doc:`python-async-client`). + +**Custom** clients call ``CuOptRemoteService`` over gRPC using these definitions. +This page summarizes the service for custom integrators and debugging. Service: ``CuOptRemoteService`` ================================ @@ -26,7 +35,7 @@ Asynchronous Jobs * - RPC - Purpose * - ``SubmitJob`` - - Submit an LP or MILP job in one message (within gRPC message size limits). + - Submit an LP or MIP job in one message (within gRPC message size limits). * - ``CheckStatus`` - Poll job status by ``job_id``. * - ``GetResult`` @@ -82,17 +91,27 @@ Streaming and Callbacks * - ``StreamLogs`` - Server-streaming solver log lines for a job. * - ``GetIncumbents`` - - MILP incumbent solutions since a given index. + - MIP incumbent solutions since a given index (only if the job was + submitted with ``enable_incumbents``; otherwise the list is empty). Messages and Constraints ======================== -* **Problem types** — LP and MILP in the enum; the problem payload can include quadratic objective data for **QP**-style solves where the client API supports it. **Routing** over this gRPC service is **not** available yet; it is planned for an **upcoming** release (use REST for remote routing today). +* **Problem types** — Wire categories are LP/QP or MIP. QP is submitted as + ``lp_request`` (``SolveLPRequest``) with quadratic fields on + ``OptimizationProblem``. **Routing** over this gRPC service is **not** + available yet (planned; use REST for remote routing today). * **Solver settings** — Carried as ``PDLPSolverSettings`` or ``MIPSolverSettings`` inside the request or chunked header, aligned with the NVIDIA cuOpt solver options documentation. -* **Errors** — gRPC status codes carry failures (see comments at the end of ``cuopt_remote_service.proto``). +* **Errors** — Transport failures use gRPC status codes. Some outcomes use + ``Status::OK`` with response fields: ``CheckStatus`` reports unknown jobs as + ``job_status=NOT_FOUND``; ``GetResult`` uses transport ``NOT_FOUND`` / + ``UNAVAILABLE`` (not ready) and ``status=ERROR_SOLVE_FAILED`` for failed + solves; ``DeleteResult`` / ``CancelJob`` report outcomes in the response. + See ``cuopt_remote_service.proto``. Further Reading =============== +* :doc:`python-async-client` / :doc:`python-async-client-api` — Python job client (``cuopt.grpc``) built on these RPCs. * :doc:`grpc-server-architecture` — Server process model and job lifecycle (overview); :doc:`advanced` for ``cuopt_grpc_server`` flags. Contributor details: ``cpp/docs/grpc-server-architecture.md``. * :doc:`advanced` — TLS, Docker, client environment variables, and limitations. diff --git a/docs/cuopt/source/cuopt-grpc/examples.rst b/docs/cuopt/source/cuopt-grpc/examples.rst index 6da8b76740..35bb1e535e 100644 --- a/docs/cuopt/source/cuopt-grpc/examples.rst +++ b/docs/cuopt/source/cuopt-grpc/examples.rst @@ -6,13 +6,18 @@ Examples ======== -gRPC remote execution uses the same **Python**, **C API**, and **cuopt_cli** entry points as a local solve. After you start ``cuopt_grpc_server`` on the GPU host (:doc:`quick-start`), set the client environment and run **any** of the examples below **unchanged** — no code edits are required. +**Remote execution** uses the same **Python**, **C API**, and **cuopt_cli** +entry points as a local solve. After you start ``cuopt_grpc_server`` on the +GPU server (:doc:`quick-start`), set the client environment and run the +integrated examples below **unchanged** — no code edits are required. The +:ref:`Python async gRPC client ` section is +separate: it uses ``Client(host, port)`` and does not read ``CUOPT_REMOTE_*``. -On the **client** host, before running the example commands or scripts: +On the **client** machine, before running the example commands or scripts: .. code-block:: bash - export CUOPT_REMOTE_HOST= + export CUOPT_REMOTE_HOST= export CUOPT_REMOTE_PORT=5001 Add TLS or tuning variables from :doc:`advanced` if your deployment uses them. @@ -24,19 +29,21 @@ Add TLS or tuning variables from :doc:`advanced` if your deployment uses them. Where to Find Examples ====================== -Python (LP / QP / MILP) +Python (LP / QP / MIP) ----------------------- * :doc:`../cuopt-python/convex/convex-examples` — runnable Python samples (LP, QP). With ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` set on the client, solves go to the remote server automatically. -* :doc:`../cuopt-python/mip/mip-examples` — runnable Python samples (MILP). With ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` set on the client, solves go to the remote server automatically. +* :doc:`../cuopt-python/mip/mip-examples` — runnable Python samples (MIP). With ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` set on the client, solves go to the remote server automatically. -C API (LP / QP / MILP) +C API (LP / QP / MIP) ---------------------- * :doc:`../cuopt-c/convex/convex-examples` — LP and QP C examples. -* :doc:`../cuopt-c/mip/mip-examples` — MILP C examples. +* :doc:`../cuopt-c/mip/mip-examples` — MIP C examples. - Compile and run these programs with the same exports in the shell; ``solve_lp`` / ``solve_mip`` use gRPC when both remote variables are set (see :doc:`../cuopt-c/convex/convex-c-api` for API reference). + Compile and run these programs with the same exports in the shell; + ``cuOptSolve`` uses gRPC when both remote variables are set (see + :doc:`../cuopt-c/convex/convex-c-api` for API reference). ``cuopt_cli`` ------------- @@ -46,15 +53,30 @@ C API (LP / QP / MILP) Minimal Demos (This Section) ---------------------------- -Bundled with the gRPC docs source for a quick copy-paste path (also walked through in :doc:`quick-start`): +Included with the gRPC docs source for a quick copy-paste path (also walked through in :doc:`quick-start`): * :download:`remote_lp_demo.py ` * :download:`remote_lp_demo.mps ` +Python Async gRPC Client +------------------------ + +.. _cuopt-grpc-examples-async-client: + +For explicit job control (submit / wait / cancel / stream logs or incumbents) +without ``CUOPT_REMOTE_*``, use ``cuopt.grpc.linear_programming.Client``: + +* :doc:`python-async-client` — overview +* :doc:`python-async-client-examples` — log streaming and incumbent streaming +* :doc:`python-async-client-api` — API reference + Custom gRPC Client ------------------ -Integrations that do **not** use the bundled Python / C / CLI stack should speak ``CuOptRemoteService`` directly. See :doc:`api`, :doc:`grpc-server-architecture`, and ``cpp/docs/grpc-server-architecture.md`` in the repository for protos and server behavior. +Integrations that do **not** use remote execution or the Python async gRPC +client should speak ``CuOptRemoteService`` directly. See :doc:`api`, +:doc:`grpc-server-architecture`, and ``cpp/docs/grpc-server-architecture.md`` +in the repository for protos and server behavior. More Samples ============ diff --git a/docs/cuopt/source/cuopt-grpc/examples/incumbent_stream_demo.py b/docs/cuopt/source/cuopt-grpc/examples/incumbent_stream_demo.py new file mode 100644 index 0000000000..60b0f456f5 --- /dev/null +++ b/docs/cuopt/source/cuopt-grpc/examples/incumbent_stream_demo.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""MIP incumbent streaming via the Python async gRPC client. + +Same ``set_mip_callback`` registration as a local solve, plus +``start_incumbent_stream`` so those callbacks fire while the remote job runs. + +Start the server first:: + + cuopt_grpc_server --port 5001 --workers 1 + +Then:: + + python incumbent_stream_demo.py +""" + +from cuopt.grpc.linear_programming import Client, JobStatus +from cuopt.linear_programming.internals import GetSolutionCallback +from cuopt.linear_programming.problem import INTEGER, MAXIMIZE, Problem +from cuopt.linear_programming.solver_settings import SolverSettings + + +class IncumbentPrinter(GetSolutionCallback): + def get_solution(self, solution, solution_cost, solution_bound, user_data): + print( + f"incumbent cost={float(solution_cost[0]):.4f} " + f"values={solution.tolist()}", + flush=True, + ) + + +problem = Problem("incumbent_stream_demo") +x = problem.addVariable(lb=0, ub=10, vtype=INTEGER, name="x") +y = problem.addVariable(lb=0, ub=10, vtype=INTEGER, name="y") +problem.addConstraint(x + y <= 10, name="c1") +problem.addConstraint(x - y >= 0, name="c2") +problem.setObjective(x + 2 * y, sense=MAXIMIZE) + +settings = SolverSettings() +settings.set_mip_callback(IncumbentPrinter(), None) +settings.set_parameter("time_limit", 30) + +client = Client("localhost", 5001) +job_id = client.submit(problem, settings) +try: + client.start_incumbent_stream(job_id, settings=settings) + if client.wait(job_id, timeout=120) != JobStatus.COMPLETED: + raise RuntimeError("job did not complete") + client.join_incumbent_stream(job_id) + names = [v.getVariableName() for v in problem.getVariables()] + solution = client.result(job_id, variable_names=names) + print(solution.get_termination_reason(), solution.get_primal_objective()) +finally: + client.delete(job_id) diff --git a/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.mps b/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.mps index 95d342250c..fc2ea93cba 100644 --- a/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.mps +++ b/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.mps @@ -1,13 +1,19 @@ -NAME good-1 +NAME good-1 +OBJSENSE + MAXIMIZE ROWS N COST L ROW1 L ROW2 COLUMNS - VAR1 COST -0.2 - VAR1 ROW1 3 ROW2 2.7 - VAR2 COST 0.1 - VAR2 ROW1 4 ROW2 10.1 + VAR1 COST 0.2 + VAR1 ROW1 3.0 ROW2 2.7 + VAR2 COST 0.1 + VAR2 ROW1 4.0 ROW2 10.1 RHS - RHS1 ROW1 5.4 ROW2 4.9 + RHS1 ROW1 5.4 ROW2 4.9 +BOUNDS + LO BND1 VAR1 0.0 + UP BND1 VAR1 2.0 + LO BND1 VAR2 0.0 ENDATA diff --git a/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.py b/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.py index 4b24938c6c..7f596a945b 100644 --- a/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.py +++ b/docs/cuopt/source/cuopt-grpc/examples/remote_lp_demo.py @@ -23,6 +23,7 @@ c = np.array([0.2, 0.1], dtype=np.float64) dm.set_objective_coefficients(c) +dm.set_maximize(True) dm.set_row_types(np.array(["L", "L"])) diff --git a/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md b/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md index 450947de6c..cc5be679aa 100644 --- a/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md +++ b/docs/cuopt/source/cuopt-grpc/grpc-server-architecture.md @@ -21,7 +21,7 @@ Implementation details (IPC layout, C++ source map, chunked transfer internals) | Topic | Detail | |-------|--------| | Log files | Per-job solver logs under `/tmp/cuopt_logs/job_.log` (used by log streaming). | -| Default caps | Up to **100** queued jobs and **100** stored results (server compile-time limits). | +| Capacity | Up to **100** in-flight jobs (queued + processing). Completed results stay until `DeleteResult` (no fixed result cap). | | Workers | Recommended: **1 worker process per GPU**. Higher values are possible depending on the problems being solved but there is no specific guidance at this time. | ## Fault tolerance and cancellation diff --git a/docs/cuopt/source/cuopt-grpc/index.rst b/docs/cuopt/source/cuopt-grpc/index.rst index 40d84b7e1e..6b272c1105 100644 --- a/docs/cuopt/source/cuopt-grpc/index.rst +++ b/docs/cuopt/source/cuopt-grpc/index.rst @@ -6,15 +6,50 @@ gRPC Remote Execution ========================== -**NVIDIA cuOpt gRPC remote execution** runs optimization solves on a remote GPU host. Clients can be the **Python** API, **C API**, **`cuopt_cli`**, or a **custom** program that speaks ``CuOptRemoteService`` over gRPC. For Python, the C API, and ``cuopt_cli``, set ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` to forward solves to ``cuopt_grpc_server``. +NVIDIA cuOpt can run LP, MIP, and QP solves on a remote GPU host through +``cuopt_grpc_server``. There are two ways to reach that server: + +**Remote execution** (zero code change) + Set ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` on the **client** machine. + The Python solver APIs, the C API ``cuOptSolve``, and ``cuopt_cli`` forward + the solve automatically. No client code changes are required. + See :doc:`quick-start`. + +**gRPC clients** (explicit client) + Your program opens a gRPC connection and manages jobs itself. Use the + :doc:`Python async gRPC client ` + (``cuopt.grpc.linear_programming.Client``) for job management, or speak + ``CuOptRemoteService`` directly from a custom client (:doc:`api`). + +In this section, **remote execution** always means the zero-code-change path +above. When talking about programs that construct a client and call gRPC +themselves, we say **gRPC client**. .. note:: - **Problem types (gRPC remote):** LP, MILP, and QP are supported today. **Routing** (VRP, TSP, PDP, and related APIs) over gRPC remote execution is **not** available yet; support is planned for an **upcoming** release. For routing against a remote service today, use the HTTP/JSON :doc:`REST self-hosted server <../cuopt-server/index>`. + **Problem types:** LP, MIP, and QP are supported today. **Routing** (VRP, + TSP, PDP, and related APIs) over gRPC is **not** available yet; support is + planned for an **upcoming** release. For remote routing today, use the + HTTP/JSON :doc:`REST self-hosted server <../cuopt-server/index>`. + +This is **not** the HTTP/JSON :doc:`REST self-hosted server <../cuopt-server/index>` +(FastAPI). REST is for arbitrary HTTP clients; gRPC serves remote execution +(client integrated into the solver APIs) and explicit gRPC clients. + +When to choose which path +========================= -This is **not** the HTTP/JSON :doc:`REST self-hosted server <../cuopt-server/index>` (FastAPI). REST is for arbitrary HTTP clients; gRPC is for the bundled remote client in NVIDIA cuOpt's native APIs. +* **Remote execution** — drop-in remote solves with no code changes; same + scripts and APIs as a local solve. +* **Python async gRPC client** — explicit job control: submit now, wait or + poll later, cancel, stream solver logs, stream MIP incumbents. +* **Custom ``CuOptRemoteService`` client** — non-Python (or fully custom) + integrations that speak the protos directly. See :doc:`api`. -Start with :doc:`quick-start` (install selector, how remote execution works, Docker, and a minimal LP example). Use :doc:`advanced` for TLS, tuning, limitations, and troubleshooting; :doc:`examples` for additional patterns. +Start with :doc:`quick-start` (install, server, and a minimal LP). Use +:doc:`python-async-client` for the Python gRPC client; :doc:`advanced` for +TLS, Docker, environment variables, and troubleshooting; :doc:`examples` for +additional patterns. .. toctree:: :maxdepth: 2 @@ -22,6 +57,9 @@ Start with :doc:`quick-start` (install selector, how remote execution works, Doc :name: cuopt-grpc-contents quick-start.rst + python-async-client.rst + python-async-client-examples.rst + python-async-client-api.rst advanced.rst examples.rst api.rst diff --git a/docs/cuopt/source/cuopt-grpc/python-async-client-api.rst b/docs/cuopt/source/cuopt-grpc/python-async-client-api.rst new file mode 100644 index 0000000000..8b031ef193 --- /dev/null +++ b/docs/cuopt/source/cuopt-grpc/python-async-client-api.rst @@ -0,0 +1,48 @@ +.. + SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + +========================================== +Python Async gRPC Client API Reference +========================================== + +Import path: ``cuopt.grpc.linear_programming``. + +Client +====== + +.. autoclass:: cuopt.grpc.linear_programming.Client + :members: + :undoc-members: + :exclude-members: _spawn_client, _as_data_model, _backfill_log_stream, _run_log_stream, _stream_logs, _run_incumbent_stream, _poll_incumbents + +Supporting Types +================ + +.. autoclass:: cuopt.grpc.linear_programming.TlsConfig + :members: + :undoc-members: + +.. autoclass:: cuopt.grpc.linear_programming.JobStatus + :members: + :undoc-members: + :member-order: bysource + :exclude-members: __new__, __init__, _generate_next_value_, as_integer_ratio, bit_count, bit_length, conjugate, denominator, from_bytes, imag, is_integer, numerator, real, to_bytes + +Exceptions +========== + +.. autoexception:: cuopt.grpc.linear_programming.GrpcError + :members: + :show-inheritance: + +.. autoexception:: cuopt.grpc.linear_programming.JobNotReadyError + :members: + :show-inheritance: + +See also +========= + +* :doc:`python-async-client` — overview and when to use this client +* :doc:`python-async-client-examples` — log and incumbent streaming examples +* :doc:`api` — ``CuOptRemoteService`` proto / RPC reference diff --git a/docs/cuopt/source/cuopt-grpc/python-async-client-examples.rst b/docs/cuopt/source/cuopt-grpc/python-async-client-examples.rst new file mode 100644 index 0000000000..aa88c48f31 --- /dev/null +++ b/docs/cuopt/source/cuopt-grpc/python-async-client-examples.rst @@ -0,0 +1,63 @@ +.. + SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + +===================================== +Python Async gRPC Client Examples +===================================== + +These snippets build on the :doc:`python-async-client` **Connect and Solve** +example. Start ``cuopt_grpc_server`` first, and pass the server host and port +to ``Client`` (not ``CUOPT_REMOTE_*``). Always call ``delete`` when finished, +and pass ``variable_names`` to ``result()`` if you want named ``get_vars()``. + +Log Streaming +============= + +After ``submit``, stream solver log lines until the job completes: + +.. code-block:: python + + from cuopt.grpc.linear_programming import Client, JobStatus + + client = Client("localhost", 5001) + job_id = client.submit(dm, settings) + try: + client.start_log_stream( + job_id, callback=lambda line, _done: print(line, flush=True) + ) + if client.wait(job_id, timeout=120) != JobStatus.COMPLETED: + raise RuntimeError("job did not complete") + + solution = client.result(job_id, variable_names=["x0", "x1"]) + print(solution.get_termination_reason(), solution.get_primal_objective()) + finally: + try: + client.join_log_stream(job_id) + finally: + client.delete(job_id) + +Incumbent Streaming (MIP) +========================= + +Register incumbent callbacks the same way as for a local solve: add a +``GetSolutionCallback`` (from ``cuopt.linear_programming.internals``) on +``SolverSettings`` with +:meth:`~cuopt.linear_programming.solver_settings.SolverSettings.set_mip_callback`. +For gRPC, pass that ``settings`` to ``submit``, then call +``start_incumbent_stream`` with the same ``settings`` so those callbacks +receive incumbents while the job runs. + +:download:`incumbent_stream_demo.py ` + +.. literalinclude:: examples/incumbent_stream_demo.py + :language: python + :linenos: + +See Also +======== + +* :doc:`python-async-client` — overview and Connect and Solve +* :doc:`python-async-client-api` — API reference +* :doc:`quick-start` — remote execution and the same LP via ``Client`` +* :doc:`examples` — remote execution examples (``CUOPT_REMOTE_*``) diff --git a/docs/cuopt/source/cuopt-grpc/python-async-client.rst b/docs/cuopt/source/cuopt-grpc/python-async-client.rst new file mode 100644 index 0000000000..2ee4d20ccd --- /dev/null +++ b/docs/cuopt/source/cuopt-grpc/python-async-client.rst @@ -0,0 +1,96 @@ +.. + SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + +============================= +Python Async gRPC Client +============================= + +The **Python async gRPC client** (``cuopt.grpc.linear_programming.Client``) +is an explicit gRPC client for ``cuopt_grpc_server``. It uses a job lifecycle: +**submit** → **wait** / **status** → **result** → **delete**. + +"Async" here means the **job-based** API (submit-and-wait / submit-and-poll). It is +**not** Python ``asyncio``. + +For **remote execution** (zero code change via ``CUOPT_REMOTE_HOST`` / +``CUOPT_REMOTE_PORT``), see :doc:`quick-start` and the section overview in +:doc:`index`. Prefer this client when you need cancel, live log streaming, +MIP incumbent streaming, or to manage multiple jobs yourself. + +Prerequisites +============= + +A running ``cuopt_grpc_server`` on a GPU host (see :doc:`quick-start`): + +.. code-block:: bash + + cuopt_grpc_server --port 5001 --workers 1 + +Connect and Solve +================= + +The LP below matches :download:`remote_lp_demo.py ` +from the quick start (same constraint matrix and objective). + +.. code-block:: python + + import numpy as np + from cuopt import linear_programming + from cuopt.grpc.linear_programming import Client, JobStatus + + dm = linear_programming.DataModel() + dm.set_csr_constraint_matrix( + np.array([3.0, 4.0, 2.7, 10.1], dtype=np.float64), + np.array([0, 1, 0, 1], dtype=np.int32), + np.array([0, 2, 4], dtype=np.int32), + ) + dm.set_constraint_bounds(np.array([5.4, 4.9], dtype=np.float64)) + dm.set_objective_coefficients(np.array([0.2, 0.1], dtype=np.float64)) + dm.set_maximize(True) + dm.set_row_types(np.array(["L", "L"])) + dm.set_variable_lower_bounds(np.array([0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds(np.array([2.0, np.inf], dtype=np.float64)) + + settings = linear_programming.SolverSettings() + client = Client("localhost", 5001) # tls=None uses CUOPT_TLS_* if set + job_id = client.submit(dm, settings) + try: + if client.wait(job_id, timeout=120) != JobStatus.COMPLETED: + raise RuntimeError("job did not complete") + # Pass names if you want solution.get_vars() keyed by name. + solution = client.result(job_id, variable_names=["x0", "x1"]) + print(solution.get_termination_reason(), solution.get_primal_objective()) + finally: + client.delete(job_id) + +``Client.submit()`` accepts either a +:class:`~cuopt.linear_programming.data_model.DataModel` or a +:class:`~cuopt.linear_programming.problem.Problem`. Always call +``delete`` after you are done with the job so the server can release state. + +Variable Names +============== + +``result(job_id, variable_names=...)`` builds the solution object. Pass a list +of variable names (same order as the model columns) if you want +``solution.get_vars()`` keyed by those names. You can omit names and still use +``get_primal_solution()`` and other numeric accessors. + +TLS / mTLS +========== + +* ``tls=None`` (default) — honor ``CUOPT_TLS_*`` environment variables. +* ``tls=False`` — plain TCP; ignore ``CUOPT_TLS_*``. +* ``tls=TlsConfig(...)`` — explicit PEM text or file paths. + +See :doc:`advanced` for server-side TLS and which environment variables apply +to remote execution versus this gRPC client. + +Next steps +========== + +* :doc:`python-async-client-examples` — log streaming and incumbent streaming +* :doc:`python-async-client-api` — API reference +* :doc:`quick-start` — start ``cuopt_grpc_server`` and remote execution +* :doc:`api` — low-level ``CuOptRemoteService`` RPCs diff --git a/docs/cuopt/source/cuopt-grpc/quick-start.rst b/docs/cuopt/source/cuopt-grpc/quick-start.rst index ae6a1d7b04..96caccd40c 100644 --- a/docs/cuopt/source/cuopt-grpc/quick-start.rst +++ b/docs/cuopt/source/cuopt-grpc/quick-start.rst @@ -6,28 +6,57 @@ Quick Start =========== -**NVIDIA cuOpt gRPC remote execution** runs LP, MILP, and QP solves on a **GPU host** while your **Python** code, **C API** program, **`cuopt_cli`**, or a **custom** client runs elsewhere. When you set ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT``, the bundled **Python**, **C API**, and **cuopt_cli** clients forward ``solve_lp`` / ``solve_mip`` to ``cuopt_grpc_server`` with **no code changes**. **Custom** clients call ``CuOptRemoteService`` directly (see :doc:`api`). +This page walks through a minimal LP against ``cuopt_grpc_server`` in two +ways: + +1. **Remote execution** — set ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` + on the client; the same Python, C (``cuOptSolve``), or ``cuopt_cli`` APIs + you use locally forward to the GPU server with **no code changes**. +2. **Python async gRPC client** — the same LP, with host and port passed to + ``Client(...)`` for explicit job control (submit / wait / result / delete). + +Start the GPU server, then try remote execution first; the gRPC-client +variant follows immediately after that demo. Full client docs: +:doc:`python-async-client`. **Custom** clients call ``CuOptRemoteService`` +directly (see :doc:`api`). .. note:: - **Problem types (gRPC remote):** **LP**, **MILP**, and **QP** are supported today. **Routing** (VRP, TSP, PDP) over this path is **not** available; For remote routing, use the HTTP/JSON :doc:`REST self-hosted server <../cuopt-server/index>`. This guide is **not** the REST server—see :doc:`../cuopt-server/index` for HTTP/JSON. + **Problem types:** **LP**, **MIP**, and **QP** are supported today. + **Routing** (VRP, TSP, PDP) over gRPC is **not** available; for remote + routing, use the HTTP/JSON :doc:`REST self-hosted server <../cuopt-server/index>`. + This guide is **not** the REST server. How Remote Execution Works ========================== -1. **GPU host** — Run ``cuopt_grpc_server`` (bare metal or in the official container) so it listens on a TCP port (default **5001**). -2. **Client** — Install the NVIDIA cuOpt client libraries on the machine where you invoke the solver. Set ``CUOPT_REMOTE_HOST`` to that GPU host’s address and ``CUOPT_REMOTE_PORT`` to the listen port. -3. **Solve** — Call the same APIs you would for a local solve. The client library opens a gRPC channel, streams the problem, and retrieves the result. Unset the two variables to solve **locally** again (local mode still needs a GPU on that machine where applicable). +1. **GPU server** — On the machine with the GPU, run ``cuopt_grpc_server`` + (bare metal or in the cuOpt container) so it listens on a TCP port + (default **5001**). +2. **Client machine** — On the machine where you invoke the solver (which may + be the same host), install the NVIDIA cuOpt client libraries. Set + ``CUOPT_REMOTE_HOST`` to the **GPU server's** hostname or IP and + ``CUOPT_REMOTE_PORT`` to the listen port. +3. **Solve** — Call the same APIs you would for a local solve. The integrated + client opens a gRPC channel, streams the problem, and retrieves the result. + Unset the two variables to solve **locally** again (local mode still needs + a GPU on the client machine where applicable). Install NVIDIA cuOpt ==================== -Use the selector below on the **GPU server** and on **clients** that need Python, the C API, or ``cuopt_cli``. It is pre-set to **C (libcuopt)** because that bundle ships ``cuopt_grpc_server``, ``cuopt_cli``, and libraries together; switch to **Python** if you only need Python packages on a lightweight client. +Use the selector below on the **GPU server** and on **client** machines that +need Python, the C API, or ``cuopt_cli``. It is pre-set to **C (libcuopt)** +because that bundle ships ``cuopt_grpc_server``, ``cuopt_cli``, and libraries +together; switch to **Python** if you only need Python packages on a +lightweight client. .. install-selector:: :default-iface: c -Verify the server binary after install: +Verify the server binary on the **GPU server** after installing the C/libcuopt +bundle (that package ships ``cuopt_grpc_server``). A Python-only client install +does not include this binary: .. code-block:: bash @@ -35,8 +64,8 @@ Verify the server binary after install: For the same install selector with **Container** / registry choices (Docker Hub or NGC), see :doc:`../install`. -Run the gRPC Server (GPU Host) -============================== +Run the gRPC Server (GPU Server) +================================ **Bare metal** — after activating the same environment you used to install NVIDIA cuOpt: @@ -44,7 +73,7 @@ Run the gRPC Server (GPU Host) cuopt_grpc_server --port 5001 --workers 1 -Leave the process running. Default port **5001**; change ``--port`` if needed and expose the same port on the client side. +Leave the process running. Default port **5001**; change ``--port`` if needed and expose the same port to the client. **Docker** — requires `NVIDIA Container Toolkit `_ (or equivalent) on the host. Pull an image tag from :doc:`../install` or the **Container** row in the selector above; substitute ```` below. @@ -68,22 +97,23 @@ Or invoke the binary explicitly: The container image defaults to the Python **REST** server when ``CUOPT_SERVER_TYPE`` is unset and you do not override the command; setting ``CUOPT_SERVER_TYPE=grpc`` selects ``cuopt_grpc_server``. Extra environment variables (``CUOPT_SERVER_PORT``, ``CUOPT_GPU_COUNT``, ``CUOPT_GRPC_ARGS``) and TLS are documented in :doc:`Advanced configuration `. -Point the Client at the Server -============================== +Minimal Python Example +====================== -On the machine where you run Python, the C API, or ``cuopt_cli`` (use ``127.0.0.1`` if the server is on the same host): +On the **client machine**, point remote execution at the GPU server (use +``127.0.0.1`` if the server is on the same host): .. code-block:: bash - export CUOPT_REMOTE_HOST= + export CUOPT_REMOTE_HOST= export CUOPT_REMOTE_PORT=5001 -Optional TLS and tuning variables are in :doc:`advanced`. +Optional TLS and tuning variables are in :doc:`advanced`. The same exports +apply to the C API and ``cuopt_cli``. -Minimal Python Example (LP) -============================ - -The script is the same for **local** or **remote** solves: with the exports above, the client library forwards to ``cuopt_grpc_server``; without them, the solve runs locally (where a GPU is available). +The script below is the same for **local** or **remote** solves: with the +exports above, the integrated client forwards to ``cuopt_grpc_server``; +without them, the solve runs locally (where a GPU is available). Please make sure the server is running before running the client. :download:`remote_lp_demo.py ` @@ -111,10 +141,48 @@ You should see an optimal termination. To solve **locally**, unset the remote va unset CUOPT_REMOTE_HOST CUOPT_REMOTE_PORT python remote_lp_demo.py +.. rubric:: Same LP via the Python Async gRPC Client + :class: large-rubric + +Remote execution needs no code changes. If you want **explicit** job control +instead, leave ``CUOPT_REMOTE_*`` unset and use the Python async gRPC client. +Pass the GPU server's network location in the ``Client`` constructor +(``host`` and ``port``); it does not read ``CUOPT_REMOTE_*``. + +Keep the ``DataModel`` setup and ``SolverSettings`` from the listing above, and +replace everything from the ``Solve`` call onward (line 33 in that listing) +with: + +.. code-block:: python + + from cuopt.grpc.linear_programming import Client, JobStatus + + # Network location of cuopt_grpc_server (not CUOPT_REMOTE_*). + client = Client("localhost", 5001) + job_id = client.submit(dm, settings) + try: + status = client.wait(job_id, timeout=120) + if status != JobStatus.COMPLETED: + raise RuntimeError(f"unexpected status: {status}") + # Pass variable names if you want solution.get_vars() keyed by name. + solution = client.result(job_id, variable_names=["x0", "x1"]) + print("Termination:", solution.get_termination_reason()) + print("Objective: ", solution.get_primal_objective()) + print("Primal x: ", solution.get_primal_solution()) + finally: + client.delete(job_id) + +A full walkthrough of log and incumbent streaming is in +:doc:`python-async-client-examples`. Overview and TLS details: +:doc:`python-async-client`. + Minimal ``cuopt_cli`` Example (LP) ================================== -The same **LP** is available as MPS. With ``CUOPT_REMOTE_HOST`` and ``CUOPT_REMOTE_PORT`` set as above, ``cuopt_cli`` forwards the solve to the remote server; unset them for a **local** run (GPU on that machine). +The same **LP** is available as MPS. With ``CUOPT_REMOTE_HOST`` and +``CUOPT_REMOTE_PORT`` set as in the Python example above, ``cuopt_cli`` +forwards the solve to the remote server; unset them for a **local** run +(GPU on that machine). Please make sure the server is running before running the client. :download:`remote_lp_demo.mps ` @@ -143,16 +211,18 @@ To solve **locally** with the same file: More options (time limits, relaxation): :doc:`../cuopt-cli/quick-start` and :doc:`examples`. -**C API** — With the same environment variables set, call ``solve_lp`` / ``solve_mip`` as in :doc:`../cuopt-c/convex/convex-c-api`. +**C API** — With the same environment variables set, call ``cuOptSolve`` as in +:doc:`../cuopt-c/convex/convex-c-api`. -More patterns (MPS variants, custom gRPC): :doc:`examples`. +More patterns: :doc:`examples`. Next Steps ========== * :doc:`../install` — Top-level install selector (all interfaces), including **Container** pulls. +* :doc:`python-async-client` — Python async gRPC client (explicit jobs). * :doc:`advanced` — TLS / mTLS, Docker environment reference, tuning, limitations, troubleshooting. -* :doc:`examples` — Additional client examples and links to LP/MILP sample collections. +* :doc:`examples` — Additional client examples and links to LP/MIP sample collections. * :doc:`api` and :doc:`grpc-server-architecture` — RPC summary and server behavior overview. See :doc:`../system-requirements` for GPU, CUDA, and OS requirements. diff --git a/docs/cuopt/source/cuopt-python/index.rst b/docs/cuopt/source/cuopt-python/index.rst index 5b4ea84da6..9ec02f14e7 100644 --- a/docs/cuopt/source/cuopt-python/index.rst +++ b/docs/cuopt/source/cuopt-python/index.rst @@ -6,6 +6,11 @@ NVIDIA cuOpt supports Python API for routing optimization, convex optimization, This section contains details on the cuOpt Python package. +For remote solves with **no code changes**, set ``CUOPT_REMOTE_HOST`` / +``CUOPT_REMOTE_PORT`` (see :doc:`../cuopt-grpc/quick-start`). For an +**explicit** Python job API against ``cuopt_grpc_server``, see the +:doc:`Python async gRPC client <../cuopt-grpc/python-async-client>`. + .. toctree:: :maxdepth: 3 :caption: Python API Overview diff --git a/docs/cuopt/source/introduction.rst b/docs/cuopt/source/introduction.rst index 1769a30bdf..3c61684316 100644 --- a/docs/cuopt/source/introduction.rst +++ b/docs/cuopt/source/introduction.rst @@ -2,14 +2,14 @@ Introduction ========================== -**NVIDIA® cuOpt™** is a GPU-accelerated optimization library that solves `Linear Programming (LP) `_, `Quadratic Programming (QP) `_, and `Vehicle Routing Problems (VRP) `_, with support for `Quadratically Constrained Quadratic Programming (QCQP) `_ (beta), `Second-Order Cone Programming (SOCP) `_ (beta), and `Mixed Integer Linear Programming (MILP) `_ (beta). It enables solutions for large-scale problems with millions of variables and constraints, offering seamless deployment across hybrid and multi-cloud environments. +**NVIDIA® cuOpt™** is a GPU-accelerated optimization library that solves `Linear Programming (LP) `_, `Quadratic Programming (QP) `_, and `Vehicle Routing Problems (VRP) `_, with support for `Quadratically Constrained Quadratic Programming (QCQP) `_ (beta), `Second-Order Cone Programming (SOCP) `_ (beta), and `Mixed Integer Programming (MIP) `_ (beta). It enables solutions for large-scale problems with millions of variables and constraints, offering seamless deployment across hybrid and multi-cloud environments. Using accelerated computing, NVIDIA® cuOpt optimizes operations research and logistics by enabling better, faster decisions. As part of `NVIDIA AI Enterprise `_, NVIDIA cuOpt offers a secure, efficient way to rapidly generate world-class route optimization solutions. Using a single optimized container, you can deploy the AI microservice in under 5 minutes on accelerated NVIDIA GPU systems in the cloud, data center, workstations, or PCs. A license for NVIDIA AI Enterprise or membership in the NVIDIA Developer Program is required. For more information about NVAIE licensing, accessing NGC registry, and pulling container images, please refer to the :doc:`FAQ section `. .. note:: - NVAIE support is extended to only cuOpt Routing service API. LP and MILP are not supported as part of it, they are just add-ons. + NVAIE support is extended to only cuOpt Routing service API. LP and MIP are not supported as part of it, they are just add-ons. .. note:: Check out this `FAQ `__ for more information about the NVIDIA Developer Program. @@ -76,10 +76,10 @@ cuOpt includes three LP solving methods: All three algorithms can be run concurrently on both GPU and CPU, with the fastest solution returned automatically. -Mixed Integer Linear Programming (MILP) (Beta) -============================================== +Mixed Integer Programming (MIP) (Beta) +====================================== -A **Mixed Integer Program (MIP)** is an optimization problem where some variables are restricted to take on only integer values, while other variables can vary continuously. A **Mixed Integer Linear Program (MILP)** is a MIP with a linear objective and linear constraints. +A **Mixed Integer Program (MIP)** is an optimization problem where some variables are restricted to take on only integer values, while other variables can vary continuously. cuOpt's MIP support covers problems with a linear objective and linear constraints. .. note:: @@ -102,12 +102,12 @@ and suppose we wish to maximize the objective function f(x,y) = 5x + 3y. -This is a mixed integer linear program. +This is a mixed integer program. -Although MILPs seems similar to a LPs, they require much more computation to solve. +Although MIPs seem similar to LPs, they require much more computation to solve. -How cuOpt Solves the Mixed-Integer Linear Programming Problem -------------------------------------------------------------- +How cuOpt Solves the Mixed-Integer Programming Problem +------------------------------------------------------ cuOpt combines GPU-accelerated primal heuristics for improving the primal bound with traditional CPU algorithms, including branch and bound, to improve the dual bound. Primal heuristics such as local search, feasibility pump, and feasibility jump run on the GPU. Integer feasible solutions are shared between these components. @@ -120,17 +120,19 @@ cuOpt supports the following APIs: - C API support - :doc:`Linear Programming (LP) / Quadratic Programming (QP) - C ` - - :doc:`Mixed Integer Linear Programming (MILP) - C ` + - :doc:`Mixed Integer Programming (MIP) - C ` - C++ API support - cuOpt is written in C++ and includes a native C++ API. However, we do not provide documentation for the C++ API at this time. We anticipate that the C++ API will change significantly in the future. Use it at your own risk. - Python support - :doc:`Routing (TSP, VRP, and PDP) - Python ` - - :doc:`Linear Programming (LP) / Quadratic Programming (QP) and Mixed Integer Linear Programming (MILP) - Python ` -- gRPC remote execution - - :doc:`Linear Programming (LP) / Quadratic Programming (QP) and Mixed Integer Linear Programming (MILP) - gRPC remote ` + - :doc:`Linear Programming (LP) / Quadratic Programming (QP) and Mixed Integer Programming (MIP) - Python ` +- gRPC remote execution and gRPC clients + - :doc:`Remote execution (zero code change) ` — set ``CUOPT_REMOTE_HOST`` / ``CUOPT_REMOTE_PORT``; Python, C (``cuOptSolve``), and ``cuopt_cli`` forward automatically + - :doc:`Python async gRPC client ` — explicit job API (submit / wait / cancel / stream logs and incumbents) + - :doc:`Custom CuOptRemoteService clients ` — speak the gRPC protos directly - Server support - :doc:`Linear Programming (LP) - Server ` - - :doc:`Mixed Integer Linear Programming (MILP) - Server ` + - :doc:`Mixed Integer Programming (MIP) - Server ` - :doc:`Routing (TSP, VRP, and PDP) - Server ` - Third-party modeling languages - `AMPL `_ diff --git a/python/cuopt/cuopt/grpc/linear_programming/grpc_client.pyx b/python/cuopt/cuopt/grpc/linear_programming/grpc_client.pyx index 323eceba73..78a72e2413 100644 --- a/python/cuopt/cuopt/grpc/linear_programming/grpc_client.pyx +++ b/python/cuopt/cuopt/grpc/linear_programming/grpc_client.pyx @@ -33,13 +33,15 @@ from cuopt.linear_programming.solver_settings.solver_settings cimport ( from enum import IntEnum import math +import threading +import time +import warnings + from libc.stdint cimport int64_t from libc.stddef cimport size_t from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move -import threading -import time import numpy as np @@ -236,6 +238,14 @@ cdef class Client: return Client(self._host, self._port, tls=self._tls) def submit(self, problem, SolverSettings settings not None): + """ + Submit a problem for solving and return its ``job_id``. + + ``problem`` is a :class:`~cuopt.linear_programming.problem.Problem` or + :class:`~cuopt.linear_programming.data_model.DataModel`. The job runs + asynchronously; use :meth:`wait` or :meth:`status` to track it and + :meth:`result` to fetch the solution. Always :meth:`delete` when done. + """ cdef DataModel data_model cdef grpc_submit_result_t submit_result cdef bint mip @@ -260,6 +270,9 @@ cdef class Client: return submit_result.job_id.decode("utf-8") def status(self, str job_id): + """ + Return the current :class:`JobStatus` for ``job_id`` without blocking. + """ cdef grpc_status_result_t status_result = self._client.get().status( job_id.encode("utf-8") ) @@ -268,6 +281,16 @@ cdef class Client: return JobStatus(status_result.status) def wait(self, str job_id, timeout=None): + """ + Block until ``job_id`` reaches a terminal state and return its + :class:`JobStatus`. + + ``timeout`` is in whole seconds. ``None`` waits indefinitely. + Non-``None`` values are converted with ``int(timeout)`` (so ``0.5`` + becomes ``0`` and waits indefinitely). Positive timeouts poll about + once per second and raise :class:`GrpcError` if the deadline expires + (they do not return a non-terminal :class:`JobStatus`). + """ cdef int timeout_seconds = 0 if timeout is None else int(timeout) cdef grpc_status_result_t wait_result = self._client.get().wait( job_id.encode("utf-8"), timeout_seconds @@ -277,11 +300,21 @@ cdef class Client: return JobStatus(wait_result.status) def cancel(self, str job_id): + """ + Request cancellation of a running job. The job moves to + :attr:`JobStatus.CANCELLED`; call :meth:`delete` to release its state. + """ cdef string error_out if not self._client.get().cancel(job_id.encode("utf-8"), error_out): raise GrpcError(error_out.decode("utf-8")) def delete(self, str job_id): + """ + Cancel ``job_id`` if it is still running, then delete it on the server + and release its state. Joins any client-side incumbent-stream thread + for this job first. Call once you no longer need the job's result or + logs. + """ if job_id in self._incumbent_threads: self.join_incumbent_stream(job_id) cdef string error_out @@ -294,7 +327,8 @@ cdef class Client: LP vs MIP is determined from the server response (via ``grpc_client_t::get_result``). Pass ``variable_names`` (column order) - to key ``solution.get_vars()`` by name. + to key ``solution.get_vars()`` by name. Raises :class:`GrpcError` if the job + failed or was cancelled. """ cdef grpc_result_outcome_t outcome cdef unique_ptr[solver_ret_t] sol_ret @@ -373,10 +407,19 @@ cdef class Client: return thread def join_log_stream(self, str job_id, timeout=None): - """Wait for a background log stream started by :meth:`start_log_stream`. + """Wait for the background log-stream thread started by :meth:`start_log_stream`. + + Returns a dict when a thread was started for ``job_id``, else ``None``. + Useful keys: - Returns a dict with stream stats (``live_lines``, ``lines``, ``backfilled``) - when a stream was started for ``job_id``, else ``None``. + * ``lines`` — list of log line strings collected so far + * ``live_lines`` — count of lines received from the live stream thread + * ``backfilled`` — ``True`` if the live stream received no lines and + this method then called :meth:`logs` as a client-side fallback to + fill ``lines`` (and re-invoke the callback). That fetch is not + destructive; the server keeps the log until :meth:`delete`. + + Other keys in the dict are internal; do not rely on them. """ thread = self._log_threads.get(job_id) if thread is not None: @@ -476,8 +519,9 @@ cdef class Client: Poll for MIP incumbent solutions on a background thread until the job completes. - Pass ``settings`` with :meth:`SolverSettings.set_mip_callback` - registered :class:`GetSolutionCallback` instances (same as local solve). + Pass ``settings`` with ``GetSolutionCallback`` instances registered via + :meth:`~cuopt.linear_programming.solver_settings.SolverSettings.set_mip_callback` + (same as local solve). Call :meth:`join_incumbent_stream` before :meth:`delete`. """ @@ -508,10 +552,16 @@ cdef class Client: return thread def join_incumbent_stream(self, str job_id, timeout=None): - """Wait for a background incumbent poll started by :meth:`start_incumbent_stream`.""" - thread = self._incumbent_threads.pop(job_id, None) + """Wait for the background incumbent-stream thread started by :meth:`start_incumbent_stream`.""" + thread = self._incumbent_threads.get(job_id) if thread is not None: thread.join(timeout) + if thread.is_alive(): + exc = self._incumbent_thread_errors.get(job_id) + if exc is not None: + raise exc + return + self._incumbent_threads.pop(job_id, None) exc = self._incumbent_thread_errors.pop(job_id, None) if exc is not None: raise exc