Skip to content

Routing over gRPC: VRP server + compiled C++/Cython client#1597

Open
ramakrishnap-nv wants to merge 10 commits into
mainfrom
routing-grpc-vrp-e2e
Open

Routing over gRPC: VRP server + compiled C++/Cython client#1597
ramakrishnap-nv wants to merge 10 commits into
mainfrom
routing-grpc-vrp-e2e

Conversation

@ramakrishnap-nv

@ramakrishnap-nv ramakrishnap-nv commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

End-to-end VRP (vehicle routing) over the existing gRPC server, using a single architecture consistent with LP/MILP (compiled C++/Cython client, no pure-Python protobuf path). Rebased on main (which now includes #1591).

  • Server (from @tmckayus's feature/grpc-server-vrp, rebased onto main): cuopt_routing.proto, vrp_request arm, server dispatch + grpc_routing_problem_mapper + cpu_routing_problem_t.
  • Client: cpu_routing_solution_t + client-side parse; submit_vrp/get_vrp_result/solve_vrp on grpc_client_t (unary-only); a grpc_python_client_t shim; and cuopt.grpc.routing.RoutingClient (Cython) that walks the DataModel store-then-build IR into cpu_routing_problem_t — the single mapping site that replaces to_host_problem (removed).

Testing

  • Coverage: test_grpc_serialization asserts every cuopt.routing._deferred._SETTERS name is mapped (fail-loud on a new setter), exercises each field category via a no-server problem_summary probe, and covers numpy / pandas / cuDF inputs.
  • End-to-end against cuopt_grpc_server: remote solve matches local (objective, route, vehicle count) for cost-matrix, capacity, and time-window problems.
  • GPU-less client: validated in a completely GPU-disconnected container (docker run without --gpus) — a local solve fails (CUDARuntimeError), while remote solves with numpy and pandas inputs succeed against a host GPU server.

VRP is unary-only (no >2 GiB chunking yet), matching the server POC.

🤖 Generated with Claude Code

@ramakrishnap-nv ramakrishnap-nv added this to the 26.08 milestone Jul 21, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

tmckayus and others added 4 commits July 21, 2026 12:28
The routing gRPC path will use one architecture, consistent with LP/MILP: a
compiled C++/Cython client that serializes a host cpu_routing_problem_t in C++
(map_routing_problem_to_proto) rather than a parallel pure-Python protobuf path.

Remove the Phase 3 Python export layer (to_host_problem / _serialize.py and its
test): the store-then-build IR is instead walked in the Cython client to fill
cpu_routing_problem_t, so the mapping lives in one place and there is no second
serializer to maintain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
A pure-Python client would need a second serialization path; instead follow the
LP/MILP pattern so routing has one architecture:

- C++: cpu_routing_solution_t + map_proto_to_routing_solution (client-side parse
  of the RoutingSolution blob); submit_vrp/get_vrp_result/solve_vrp on
  grpc_client_t (unary-only, mirroring submit_lp); accept routing_solution in the
  unary GetResult path.
- cython shim (grpc_python_client_t): submit_vrp/result_vrp exposing the plain
  host structs to Cython (no grpc/protobuf in the extension build).
- Cython client (cuopt.grpc.routing.RoutingClient): walks the DataModel
  store-then-build IR to fill cpu_routing_problem_t -- the single mapping site
  that replaces to_host_problem -- then drives submit/wait/result and returns the
  parsed solution.

Validated end-to-end against cuopt_grpc_server: remote solve matches local
(objective, route, vehicle count) for cost-matrix, capacity, and time-window
problems.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
problem_summary() runs the exact _populate path without a server; the tests
assert every cuopt.routing._deferred._SETTERS name is mapped (fail-loud on a
new setter), exercise each field category, cover numpy/pandas/cuDF inputs, and
check the fail-loud KeyError on an unmapped setter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv ramakrishnap-nv self-assigned this Jul 21, 2026
@ramakrishnap-nv ramakrishnap-nv added non-breaking Introduces a non-breaking change Feature improvement Improves an existing functionality labels Jul 21, 2026
@ramakrishnap-nv
ramakrishnap-nv marked this pull request as ready for review July 21, 2026 17:30
@ramakrishnap-nv
ramakrishnap-nv requested review from a team as code owners July 21, 2026 17:30
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds unary VRP gRPC schemas, C++ server/client execution, host-to-device routing conversion, Cython/Python bindings, a standalone C++ driver, and Python serialization/integration tests. The previous Python host serializer and its tests are removed.

Changes

Unary VRP gRPC integration

Layer / File(s) Summary
VRP contracts and public data models
cpp/src/grpc/cuopt_routing.proto, cpp/src/grpc/*proto, cpp/src/grpc/codegen/*, cpp/include/cuopt/routing/*, cpp/include/cuopt/grpc/*, cpp/src/grpc/client/grpc_client.hpp, cpp/CMakeLists.txt
Defines routing requests, solver settings, solutions, host-side problem/solution containers, VRP result APIs, enum conversion behavior, and protobuf generation wiring.
Routing conversion and device preparation
cpp/src/grpc/grpc_routing_problem_mapper.*, cpp/src/routing/cpu_routing_problem.cu, cpp/src/routing/CMakeLists.txt
Maps routing protobufs to host structures and materializes routing inputs into device-backed data model views.
Unary VRP server execution
cpp/src/grpc/server/*
Recognizes VRP jobs, rejects chunked VRP uploads, dispatches routing solves, and returns serialized routing solutions.
C++ and Python client workflow
cpp/src/grpc/client/*, python/cuopt/cuopt/grpc/routing/*, python/cuopt/cuopt/grpc/CMakeLists.txt
Adds C++ and Cython APIs for submission, waiting, result retrieval, deletion, and combined remote solving, including Python-side problem and solution conversion.
VRP drivers and regression coverage
cpp/tests/routing/grpc/*, python/cuopt/cuopt/tests/routing/test_grpc_*.py
Adds a standalone gRPC driver plus serialization and live-server tests for routing workflows.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: iroy30, jameslamb, kh4ster, yuwenchen95, tmckayus

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: VRP support over gRPC with compiled C++/Cython client code.
Description check ✅ Passed The description is directly related to the VRP gRPC server/client changes and matches the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch routing-grpc-vrp-e2e

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

CI Test Summary

✅ All 31 test job(s) passed.

- Remove grpc_client_t::solve_vrp: the Cython client drives submit/wait/result
  through the shim, so the C++ one-shot convenience had no caller.
- Trim unused .pxd declarations (TLS options, status(), last_error(), and
  unused result fields) to only what the client uses.
- _add_matrix: drop the redundant to_host/astype/ascontiguousarray/ravel that
  _fill_f32 already performs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/src/grpc/server/grpc_worker.cpp (1)

570-577: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

VRP result size isn't accounted for, defeating the oversized-result guard.

result_total_bytes only sums sr.arrays, but run_vrp_solve (lines 535-565) never populates sr.arrays — the routing solution bytes live in sr.header.routing_solution(). For VRP jobs this leaves result_total_bytes == 0, so result.data_size becomes std::max(0, 1) == 1 regardless of how large the actual serialized RoutingSolution is.

This silently breaks the size guard in grpc_service_impl.cpp's GetResult (the if (max_bytes > 0 && total_result_bytes > max_bytes) check around line 353), which relies on this same result_size_bytes to redirect oversized results to chunked download with a clear message. Since chunked VRP download is unsupported (rejected both client- and server-side), a large VRP solution will instead attempt to be sent as a single oversized unary gRPC response and fail with an opaque transport-level error instead of the intended graceful RESOURCE_EXHAUSTED message.

🐛 Suggested fix
   int64_t result_total_bytes = 0;
   if (sr.success) {
     for (const auto& [fid, data] : sr.arrays) {
       result_total_bytes += data.size();
     }
+    if (sr.header.is_vrp()) {
+      result_total_bytes += static_cast<int64_t>(sr.header.routing_solution().size());
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/grpc/server/grpc_worker.cpp` around lines 570 - 577, Update
publish_result to include the serialized routing solution size from
sr.header.routing_solution() when calculating result_total_bytes, while
preserving the existing sr.arrays accounting for non-VRP results. Ensure
result_size_bytes reflects the actual VRP payload so grpc_service_impl.cpp’s
GetResult oversized-result guard can return the intended RESOURCE_EXHAUSTED
response.
🧹 Nitpick comments (7)
cpp/include/cuopt/routing/cpu_routing_problem.hpp (1)

63-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM overall; minor doc gap on to_device().

Struct layout and RAII-friendly device_data_ptr pattern are solid. The to_device() docstring doesn't mention that it throws std::invalid_argument for null handle, non-positive dimensions, empty cost matrices, or incomplete capacity/break/uniform-break data (per the .cu implementation). Callers like run_vrp_solve in grpc_worker.cpp catch broad exception types, so documenting the specific failure conditions here would help future maintainers reason about error handling without cross-referencing the .cu file.

As per path instructions, "New public functions/classes need Doxygen-style documentation" and to "Suggest documenting thread-safety, GPU requirements, numerical behavior" for public headers under cpp/include/cuopt/**/*.

📝 Suggested doc improvement
   /**
-   * `@brief` Copy host data to the GPU and return a non-owning data_model_view_t.
-   * The returned device_data_t must outlive the view.
+   * `@brief` Copy host data to the GPU and return a non-owning data_model_view_t.
+   *
+   * `@param` handle Non-null RAFT handle whose stream is used for all H2D copies.
+   * `@return` A pair of the device-backed view and the owning device buffers;
+   *         the returned device_data_t must outlive the view.
+   * `@throws` std::invalid_argument if handle is null, num_locations/fleet_size
+   *         are non-positive, cost_matrices is empty, or any dependent
+   *         array group (capacity dimensions, uniform breaks) is incomplete.
    */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/cuopt/routing/cpu_routing_problem.hpp` around lines 63 - 124,
Update the Doxygen comment for cpu_routing_problem_t::to_device to document that
it requires a valid GPU handle and throws std::invalid_argument for a null
handle, non-positive dimensions, empty cost matrices, or incomplete capacity,
break, and uniform-break data. Retain the existing ownership and view-lifetime
documentation, and mention any relevant GPU/thread-safety or numerical behavior
only if established by the implementation.

Source: Path instructions

cpp/src/grpc/grpc_routing_problem_mapper.cpp (1)

24-32: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Switch to bulk RepeatedField insertion. dst->Add(src.begin(), src.end()) is supported and avoids the per-element loop for large vectors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/grpc/grpc_routing_problem_mapper.cpp` around lines 24 - 32, Update
copy_vector_to_repeated to replace the per-element Add loop with the bulk
RepeatedField insertion using src.begin() and src.end(), while preserving the
existing Clear and Reserve behavior.
python/cuopt/cuopt/grpc/routing/grpc_client.pyx (3)

363-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

wait() discards error_message, so solve()'s failure message loses the actual reason.

wait() only returns <int>st.status; st.error_message/st.success are dropped. On a timeout or connection failure, solve() (388-390) raises f"job {job_id} did not complete (status {status})", which shows only the numeric status and not the underlying reason (e.g., "Timeout waiting for job completion" vs. "Not connected to server") that the C++ layer already computed.

Also applies to: 383-396

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/grpc/routing/grpc_client.pyx` around lines 363 - 369,
Update grpc_client.wait to preserve and return the complete wait result,
including st.status, st.success, and st.error_message, then update solve to
consume that result and include the underlying error message when raising for
timeout or connection failure while preserving the existing successful
completion behavior.

324-396: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Blocking gRPC calls hold the GIL for the whole call (including long solves).

connect, submit, wait, result, and delete all call into grpc_python_client_t methods that can block for the entire RPC/poll duration (notably wait, which can run up to timeout seconds spanning the full solve). None of these are wrapped in with nogil:, so the GIL is held the whole time, starving any other Python threads in the process. Per path instructions, blocking C calls should release the GIL unless it's actually needed.

Fixing this requires marking the corresponding .pxd extern declarations nogil and precomputing any Python-dependent inputs (e.g., job_id.encode(...)) before entering the with nogil: block.

♻️ Example for wait()
     def wait(self, str job_id, int timeout=0):
         """Block until the job finishes; return the terminal status int."""
-        cdef grpc_status_result_t st = self._client.get().wait(
-            job_id.encode("utf-8"), timeout
-        )
+        cdef string job_id_cpp = job_id.encode("utf-8")
+        cdef grpc_status_result_t st
+        with nogil:
+            st = self._client.get().wait(job_id_cpp, timeout)
         return <int>st.status

(and mark wait(...) except + nogil in grpc_client.pxd)

As per path instructions, "Use `nogil` on blocking C calls unless Python GIL access is required."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/grpc/routing/grpc_client.pyx` around lines 324 - 396,
Release the GIL around the blocking grpc_python_client_t calls in __cinit__,
submit, wait, result, and delete, while computing Python-dependent values such
as encoded job IDs before each nogil block. Mark the corresponding extern
methods in grpc_client.pxd as except + nogil, and preserve existing error
handling and return behavior.

Source: Path instructions


296-317: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

_solution_to_py copies the whole solution struct by value.

cdef _solution_to_py(cpu_routing_solution_t s) takes s by value, so every call (_solution_to_py(out.solution) at line 377) performs a full deep copy of all vectors/maps in cpu_routing_solution_t (route, truck_id, locations, node_types, arrival_stamp, objective_values, etc.) before the function even starts converting to numpy. For larger VRP solutions this is avoidable overhead.

♻️ Proposed fix — take by reference
-cdef _solution_to_py(cpu_routing_solution_t s):
+cdef _solution_to_py(cpu_routing_solution_t& s):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/grpc/routing/grpc_client.pyx` around lines 296 - 317,
Change _solution_to_py to accept cpu_routing_solution_t by const reference
instead of by value, preserving all existing field conversions and return
behavior while avoiding a deep copy when _solution_to_py(out.solution) is
called.
cpp/tests/routing/grpc/grpc_vrp_test_driver.cpp (1)

227-228: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Test driver channel has no message-size limits, unlike the production client.

grpc_client_t::connect() explicitly raises SetMaxReceiveMessageSize/SetMaxSendMessageSize from config_.max_message_bytes (up to ~2GiB). This driver's grpc::CreateChannel(...) uses default ChannelArguments, so any moderately large routing JSON dataset used for manual testing will hit gRPC's default (~4MB) message-size ceiling and fail with an opaque RESOURCE_EXHAUSTED.

🔧 Proposed fix
-    auto channel = grpc::CreateChannel(server, grpc::InsecureChannelCredentials());
+    grpc::ChannelArguments args;
+    args.SetMaxReceiveMessageSize(-1);
+    args.SetMaxSendMessageSize(-1);
+    auto channel =
+      grpc::CreateCustomChannel(server, grpc::InsecureChannelCredentials(), args);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/routing/grpc/grpc_vrp_test_driver.cpp` around lines 227 - 228,
Update the channel setup in the test driver to use grpc::ChannelArguments with
maximum receive and send message sizes configured to match
grpc_client_t::connect(), using the driver's applicable max-message-bytes
configuration (up to the production limit), then pass those arguments to
grpc::CreateCustomChannel instead of the default CreateChannel.
cpp/src/grpc/client/grpc_client.cpp (1)

769-793: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No pre-flight size check before unary VRP submission.

submit_lp/submit_mip estimate proto size and fall back to chunked upload when it exceeds chunked_array_threshold_bytes_. submit_vrp always goes straight to submit_unary with no size estimate — consistent with VRP being unary-only per the PR description, but it means a large routing problem (many locations/vehicles/cost-matrix entries) will fail deep inside the RPC with an opaque RESOURCE_EXHAUSTED/serialization error rather than a clear, early message telling the caller the problem exceeds the unsupported-chunking limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/grpc/client/grpc_client.cpp` around lines 769 - 793, Add a pre-flight
serialized-size check in grpc_client_t::submit_vrp after building submit_request
and before submit_unary. Reuse the existing size-estimation and
chunked_array_threshold_bytes_ logic used by submit_lp/submit_mip; if the VRP
request exceeds the threshold, return unsuccessfully with a clear error stating
that chunked upload is unsupported for VRP, otherwise preserve the current unary
submission flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/grpc/client/cython_grpc_client.cpp`:
- Around line 171-183: Update grpc_python_client_t::result_vrp to perform the
same in-flight status check as result(), setting
grpc_vrp_result_outcome_t::not_ready and returning before calling get_vrp_result
when the job is still running. Preserve the existing remote error and successful
solution handling for completed jobs.

In `@cpp/src/grpc/client/grpc_client.cpp`:
- Around line 824-845: Update grpc_client_t::solve_vrp to wrap
poll_for_completion with the same start_log_streaming, drain_log_streaming, and
stop_log_streaming lifecycle used by solve_lp and solve_mip. Preserve the
existing success/error handling and ensure streaming is applied according to
config_.stream_logs and config_.log_callback.

In `@cpp/src/grpc/grpc_routing_problem_mapper.cpp`:
- Around line 304-332: Sanitize the error text in map_routing_solution_to_proto
before assigning it to pb->set_error_message, matching the established
format_cuopt_error behavior used by the gRPC server. Parse cuopt::logic_error
structured payloads into a clean type-and-message string and provide a safe
fallback for unrecognized or failed parsing, while preserving the existing
generic routing solve error fallback.

In `@cpp/src/routing/cpu_routing_problem.cu`:
- Around line 81-87: Update copy_u8_as_bool() and the types temporary feeding
cuopt::device_copy(...) so their host buffers remain alive until stream
completion; ensure their lifetimes extend through handle->sync_stream() rather
than ending immediately after queuing the H2D transfer.
- Around line 91-126: The to_device method must validate capacity buffer sizes
before constructing data_model_view_t: ensure each capacity dimension contains
exactly num_orders demand values and fleet_size capacity values before
populate_demand_container can consume them. Add these checks in
cpu_routing_problem_t::to_device alongside the existing input validation,
rejecting undersized or mismatched buffers before building the device view.

In `@python/cuopt/cuopt/grpc/routing/grpc_client.pyx`:
- Around line 336-348: Update _apply_settings so both dictionary and
object-based time_limit handling checks whether tl is None rather than relying
on truthiness, ensuring an explicit value of 0 is converted and passed to
s.set_time_limit while an absent value remains ignored.

---

Outside diff comments:
In `@cpp/src/grpc/server/grpc_worker.cpp`:
- Around line 570-577: Update publish_result to include the serialized routing
solution size from sr.header.routing_solution() when calculating
result_total_bytes, while preserving the existing sr.arrays accounting for
non-VRP results. Ensure result_size_bytes reflects the actual VRP payload so
grpc_service_impl.cpp’s GetResult oversized-result guard can return the intended
RESOURCE_EXHAUSTED response.

---

Nitpick comments:
In `@cpp/include/cuopt/routing/cpu_routing_problem.hpp`:
- Around line 63-124: Update the Doxygen comment for
cpu_routing_problem_t::to_device to document that it requires a valid GPU handle
and throws std::invalid_argument for a null handle, non-positive dimensions,
empty cost matrices, or incomplete capacity, break, and uniform-break data.
Retain the existing ownership and view-lifetime documentation, and mention any
relevant GPU/thread-safety or numerical behavior only if established by the
implementation.

In `@cpp/src/grpc/client/grpc_client.cpp`:
- Around line 769-793: Add a pre-flight serialized-size check in
grpc_client_t::submit_vrp after building submit_request and before submit_unary.
Reuse the existing size-estimation and chunked_array_threshold_bytes_ logic used
by submit_lp/submit_mip; if the VRP request exceeds the threshold, return
unsuccessfully with a clear error stating that chunked upload is unsupported for
VRP, otherwise preserve the current unary submission flow.

In `@cpp/src/grpc/grpc_routing_problem_mapper.cpp`:
- Around line 24-32: Update copy_vector_to_repeated to replace the per-element
Add loop with the bulk RepeatedField insertion using src.begin() and src.end(),
while preserving the existing Clear and Reserve behavior.

In `@cpp/tests/routing/grpc/grpc_vrp_test_driver.cpp`:
- Around line 227-228: Update the channel setup in the test driver to use
grpc::ChannelArguments with maximum receive and send message sizes configured to
match grpc_client_t::connect(), using the driver's applicable max-message-bytes
configuration (up to the production limit), then pass those arguments to
grpc::CreateCustomChannel instead of the default CreateChannel.

In `@python/cuopt/cuopt/grpc/routing/grpc_client.pyx`:
- Around line 363-369: Update grpc_client.wait to preserve and return the
complete wait result, including st.status, st.success, and st.error_message,
then update solve to consume that result and include the underlying error
message when raising for timeout or connection failure while preserving the
existing successful completion behavior.
- Around line 324-396: Release the GIL around the blocking grpc_python_client_t
calls in __cinit__, submit, wait, result, and delete, while computing
Python-dependent values such as encoded job IDs before each nogil block. Mark
the corresponding extern methods in grpc_client.pxd as except + nogil, and
preserve existing error handling and return behavior.
- Around line 296-317: Change _solution_to_py to accept cpu_routing_solution_t
by const reference instead of by value, preserving all existing field
conversions and return behavior while avoiding a deep copy when
_solution_to_py(out.solution) is called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2bd3d9dd-5ab6-4aee-94e7-6c2f8e08d89c

📥 Commits

Reviewing files that changed from the base of the PR and between d856342 and bca7e01.

⛔ Files ignored due to path filters (2)
  • cpp/src/grpc/codegen/generated/cuopt_remote_data.proto is excluded by !**/generated/**
  • cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc is excluded by !**/generated/**
📒 Files selected for processing (28)
  • cpp/CMakeLists.txt
  • cpp/include/cuopt/grpc/cython_grpc_client.hpp
  • cpp/include/cuopt/routing/cpu_routing_problem.hpp
  • cpp/src/grpc/client/cython_grpc_client.cpp
  • cpp/src/grpc/client/grpc_client.cpp
  • cpp/src/grpc/client/grpc_client.hpp
  • cpp/src/grpc/codegen/field_registry.yaml
  • cpp/src/grpc/cuopt_remote.proto
  • cpp/src/grpc/cuopt_remote_service.proto
  • cpp/src/grpc/cuopt_routing.proto
  • cpp/src/grpc/grpc_routing_problem_mapper.cpp
  • cpp/src/grpc/grpc_routing_problem_mapper.hpp
  • cpp/src/grpc/server/grpc_service_impl.cpp
  • cpp/src/grpc/server/grpc_worker.cpp
  • cpp/src/routing/CMakeLists.txt
  • cpp/src/routing/cpu_routing_problem.cu
  • cpp/tests/routing/CMakeLists.txt
  • cpp/tests/routing/grpc/CMakeLists.txt
  • cpp/tests/routing/grpc/grpc_vrp_test_driver.cpp
  • python/cuopt/cuopt/grpc/CMakeLists.txt
  • python/cuopt/cuopt/grpc/routing/CMakeLists.txt
  • python/cuopt/cuopt/grpc/routing/__init__.py
  • python/cuopt/cuopt/grpc/routing/grpc_client.pxd
  • python/cuopt/cuopt/grpc/routing/grpc_client.pyx
  • python/cuopt/cuopt/routing/_serialize.py
  • python/cuopt/cuopt/tests/routing/test_grpc_client.py
  • python/cuopt/cuopt/tests/routing/test_grpc_serialization.py
  • python/cuopt/cuopt/tests/routing/test_serialize.py
💤 Files with no reviewable changes (2)
  • python/cuopt/cuopt/routing/_serialize.py
  • python/cuopt/cuopt/tests/routing/test_serialize.py

Comment thread cpp/src/grpc/client/cython_grpc_client.cpp
Comment thread cpp/src/grpc/client/grpc_client.cpp Outdated
Comment on lines +824 to +845
remote_vrp_result_t grpc_client_t::solve_vrp(
const cuopt::routing::cpu_routing_problem_t& problem,
const cuopt::routing::solver_settings_t<int, float>& settings)
{
remote_vrp_result_t result;

auto sub = submit_vrp(problem, settings);
if (!sub.success) {
result.error_message = sub.error_message;
return result;
}

auto poll = poll_for_completion(sub.job_id);
if (!poll.completed) {
result.error_message = poll.error_message;
return result;
}

result = get_vrp_result(sub.job_id);
if (result.success) { delete_job(sub.job_id); }
return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

solve_vrp drops log streaming that solve_lp/solve_mip provide.

solve_lp (956-964) and solve_mip (989-995) wrap poll_for_completion with start_log_streaming/drain_log_streaming/stop_log_streaming. solve_vrp calls poll_for_completion directly with none of these, so a caller with config_.stream_logs/config_.log_callback set gets silent VRP solves while LP/MIP solves stream logs — an inconsistent, easy-to-miss feature gap.

🔧 Proposed fix to add log streaming parity
 remote_vrp_result_t grpc_client_t::solve_vrp(
   const cuopt::routing::cpu_routing_problem_t& problem,
   const cuopt::routing::solver_settings_t<int, float>& settings)
 {
   remote_vrp_result_t result;

   auto sub = submit_vrp(problem, settings);
   if (!sub.success) {
     result.error_message = sub.error_message;
     return result;
   }

+  start_log_streaming(sub.job_id);
   auto poll = poll_for_completion(sub.job_id);
+  if (poll.completed) {
+    drain_log_streaming();
+  } else {
+    stop_log_streaming();
+  }
   if (!poll.completed) {
     result.error_message = poll.error_message;
     return result;
   }

   result = get_vrp_result(sub.job_id);
   if (result.success) { delete_job(sub.job_id); }
   return result;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/grpc/client/grpc_client.cpp` around lines 824 - 845, Update
grpc_client_t::solve_vrp to wrap poll_for_completion with the same
start_log_streaming, drain_log_streaming, and stop_log_streaming lifecycle used
by solve_lp and solve_mip. Preserve the existing success/error handling and
ensure streaming is applied according to config_.stream_logs and
config_.log_callback.

Comment thread cpp/src/grpc/grpc_routing_problem_mapper.cpp
Comment thread cpp/src/routing/cpu_routing_problem.cu
Comment thread cpp/src/routing/cpu_routing_problem.cu
Comment thread python/cuopt/cuopt/grpc/routing/grpc_client.pyx
- _apply_settings: use `is not None` so an explicit time_limit=0 is applied
  rather than silently dropped (0 is falsy).
- result_vrp: add the in-flight status pre-check that result() does, so polling
  result() on a running job returns a structured not_ready signal (RoutingClient
  .result() now returns None) instead of a generic GetResult failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@ramakrishnap-nv

ramakrishnap-nv commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review — all findings addressed.

Client (002c971):

  • Falsy-zero time_limit=0 (grpc_client.pyx): _apply_settings now uses is not None, so an explicit 0 is applied instead of silently dropped.
  • result_vrp not_ready (cython_grpc_client.cpp): added the same in-flight status() pre-check as result(), so polling result() on a running job returns a structured not-ready signal (RoutingClient.result() returns None) instead of a generic GetResult failure. Re-added not_ready to the .pxd.

Server (7a61e65):

  • Error-message leak (grpc_routing_problem_mapper.cpp): map_routing_solution_to_proto now runs get_error_status() through a sanitizer that mirrors format_cuopt_error (parses the structured {"error_type","msg"} payload to type: msg), so raw internal detail isn't sent to clients. Inlined rather than cross-including the heavy server/grpc_server_types.hpp into the client-linked mapper.
  • to_device host-buffer lifetime (cpu_routing_problem.cu): the transient host buffers in copy_u8_as_bool() and the initial-solutions types conversion feed async H2D copies; the stream is now drained before they go out of scope, so the copies can't read freed memory.
  • to_device capacity validation (cpu_routing_problem.cu): each capacity dimension is now checked for num_orders demand and fleet_size capacity values before building the device view, preventing an OOB read from an undersized (e.g. raw-proto) request.

Stale: the solve_vrp log-streaming comment is moot — solve_vrp was removed earlier (no caller).

Verified: clean rebuild; valid capacity/bool-array/time-window solves match locally; undersized capacity is rejected; 8 serialization tests + GPU-less container path still pass. Full async parity (status/cancel/live log streaming) remains a planned follow-up.

ramakrishnap-nv and others added 4 commits July 21, 2026 13:40
…e + validate capacity sizes

- map_routing_solution_to_proto: run get_error_status() through a sanitizer
  (mirrors format_cuopt_error) so raw internal error text is not sent to remote
  clients, without pulling the heavy server header into the client-linked mapper.
- to_device: drain the stream before the transient host buffers in
  copy_u8_as_bool() and the initial-solutions types conversion go out of scope,
  so their async H2D copies do not read freed memory.
- to_device: validate each capacity dimension has num_orders demand and
  fleet_size capacity values before building the device view, preventing an
  out-of-bounds read from an undersized (e.g. raw-proto) request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
The VRP server commit hand-edited two generated files, which broke the
"verify gRPC codegen" check. Make both codegen-driven:

- chunked_result_header: support scalars declared directly on the section (emit
  into the proto verbatim, no LP/MIP conversion). Declares is_vrp/routing_solution.
- enum codegen: support per-value divergence between the C++ and proto enums --
  `proto_only` (proto value with no C++ enumerator; from_proto throws) and
  `cpp_aliases` (C++-only enumerator serialized as another proto value). Declares
  problem_category as VRP (proto-only) + IP->MIP, so problem_category_t (LP/MIP/IP)
  and ProblemCategory (LP/MIP/VRP) stay decoupled without hand-editing the output.

Regenerated files now match the registry; verify_grpc_codegen.sh passes and the
server/converter compile. Other enums regenerate byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
tests/routing/test_grpc_client.py collided with the existing
tests/linear_programming/test_grpc_client.py under pytest's default prepend
import mode (no __init__.py in the test dirs), causing an import-file-mismatch
collection error when the whole tests tree is collected (wheel tests). Give the
routing grpc tests routing-prefixed basenames so they are unique.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants