From b71a811664569de8e23a37eeaaa4171b9d33883e Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 15 Apr 2026 14:21:12 -0400 Subject: [PATCH 01/15] add code generation for grpc boilerplate conversions --- build.sh | 15 +- ci/test_cpp.sh | 3 + ci/verify_codegen.sh | 57 + .../all_cuda-129_arch-aarch64.yaml | 2 + .../all_cuda-129_arch-x86_64.yaml | 2 + .../all_cuda-131_arch-aarch64.yaml | 2 + .../all_cuda-131_arch-x86_64.yaml | 2 + conda/recipes/libcuopt/recipe.yaml | 2 + cpp/CMakeLists.txt | 39 +- .../cpu_optimization_problem.hpp | 12 + cpp/src/grpc/codegen/field_registry.yaml | 679 +++++ cpp/src/grpc/codegen/generate_conversions.py | 2504 +++++++++++++++++ .../codegen/generated/cuopt_remote_data.proto | 287 ++ .../generated_array_field_element_size.inc | 17 + .../generated_build_array_chunks.inc | 80 + .../generated_chunked_arrays_to_problem.inc | 124 + .../generated_chunked_header_to_problem.inc | 18 + .../generated_chunked_to_lp_solution.inc | 42 + .../generated_chunked_to_mip_solution.inc | 18 + .../generated/generated_collect_lp_arrays.inc | 24 + .../generated_collect_mip_arrays.inc | 8 + .../generated_enum_converters_problem.inc | 39 + .../generated_enum_converters_settings.inc | 49 + .../generated_enum_converters_solution.inc | 63 + .../generated/generated_estimate_lp_size.inc | 22 + .../generated/generated_estimate_mip_size.inc | 8 + .../generated_estimate_problem_size.inc | 23 + .../generated/generated_lp_chunked_header.inc | 41 + .../generated_lp_solution_to_proto.inc | 43 + .../generated_mip_chunked_header.inc | 19 + .../generated_mip_settings_to_proto.inc | 39 + .../generated_mip_solution_to_proto.inc | 19 + .../generated_pdlp_settings_to_proto.inc | 38 + .../generated_populate_chunked_header_lp.inc | 15 + .../generated_populate_chunked_header_mip.inc | 16 + .../generated/generated_problem_to_proto.inc | 70 + .../generated_proto_to_lp_solution.inc | 42 + .../generated_proto_to_mip_settings.inc | 37 + .../generated_proto_to_mip_solution.inc | 18 + .../generated_proto_to_pdlp_settings.inc | 36 + .../generated/generated_proto_to_problem.inc | 68 + .../generated_result_enums.proto.inc | 27 + cpp/src/grpc/cuopt_remote.proto | 305 +- cpp/src/grpc/cuopt_remote_data.proto | 287 ++ cpp/src/grpc/cuopt_remote_service.proto | 56 +- cpp/src/grpc/grpc_problem_mapper.cpp | 650 +---- cpp/src/grpc/grpc_settings_mapper.cpp | 253 +- cpp/src/grpc/grpc_solution_mapper.cpp | 579 +--- cpp/src/grpc/grpc_solution_mapper.hpp | 28 - .../grpc/server/grpc_field_element_size.hpp | 26 +- cpp/src/grpc/server/grpc_incumbent_proto.hpp | 2 +- .../linear_programming/grpc/CMakeLists.txt | 2 + .../grpc/grpc_pipe_serialization_test.cpp | 18 +- dependencies.yaml | 3 + docs/cuopt/grpc/GRPC_CODE_GENERATION.md | 712 +++++ docs/cuopt/grpc/GRPC_INTERFACE.md | 133 + 56 files changed, 5944 insertions(+), 1779 deletions(-) create mode 100755 ci/verify_codegen.sh create mode 100644 cpp/src/grpc/codegen/field_registry.yaml create mode 100644 cpp/src/grpc/codegen/generate_conversions.py create mode 100644 cpp/src/grpc/codegen/generated/cuopt_remote_data.proto create mode 100644 cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_build_array_chunks.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_chunked_header_to_problem.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_chunked_to_mip_solution.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_collect_lp_arrays.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_collect_mip_arrays.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_estimate_lp_size.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_estimate_mip_size.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_estimate_problem_size.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_lp_chunked_header.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_lp_solution_to_proto.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_mip_chunked_header.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_mip_solution_to_proto.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_populate_chunked_header_lp.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_populate_chunked_header_mip.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_problem_to_proto.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_proto_to_lp_solution.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_proto_to_mip_solution.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc create mode 100644 cpp/src/grpc/codegen/generated/generated_result_enums.proto.inc create mode 100644 cpp/src/grpc/cuopt_remote_data.proto create mode 100644 docs/cuopt/grpc/GRPC_CODE_GENERATION.md create mode 100644 docs/cuopt/grpc/GRPC_INTERFACE.md diff --git a/build.sh b/build.sh index d07faea237..7e647ac714 100755 --- a/build.sh +++ b/build.sh @@ -15,10 +15,11 @@ REPODIR=$(cd "$(dirname "$0")"; pwd) LIBCUOPT_BUILD_DIR=${LIBCUOPT_BUILD_DIR:=${REPODIR}/cpp/build} LIBMPS_PARSER_BUILD_DIR=${LIBMPS_PARSER_BUILD_DIR:=${REPODIR}/cpp/libmps_parser/build} -VALIDARGS="clean libcuopt cuopt_grpc_server libmps_parser cuopt_mps_parser cuopt cuopt_server cuopt_sh_client docs deb -a -b -g -fsanitize -tsan -msan -v -l= --verbose-pdlp --build-lp-only --no-fetch-rapids --skip-c-python-adapters --skip-tests-build --skip-routing-build --skip-grpc-build --skip-fatbin-write --host-lineinfo [--cmake-args=\\\"\\\"] [--cache-tool=] -n --allgpuarch --ci-only-arch --show_depr_warn -h --help" +VALIDARGS="clean codegen libcuopt cuopt_grpc_server libmps_parser cuopt_mps_parser cuopt cuopt_server cuopt_sh_client docs deb -a -b -g -fsanitize -tsan -msan -v -l= --verbose-pdlp --build-lp-only --no-fetch-rapids --skip-c-python-adapters --skip-tests-build --skip-routing-build --skip-grpc-build --skip-fatbin-write --host-lineinfo [--cmake-args=\\\"\\\"] [--cache-tool=] -n --allgpuarch --ci-only-arch --show_depr_warn -h --help" HELP="$0 [ ...] [ ...] where is: clean - remove all existing build artifacts and configuration (start over) + codegen - regenerate gRPC .inc files and proto from field_registry.yaml (requires pyyaml) libcuopt - build the cuopt C++ code cuopt_grpc_server - build only the gRPC server binary (configures + builds libcuopt as needed) libmps_parser - build the libmps_parser C++ code @@ -364,6 +365,18 @@ if buildAll || hasArg libmps_parser; then fi fi +################################################################################ +# Regenerate gRPC codegen .inc files from the field registry (explicit target only) +if hasArg codegen; then + echo "Regenerating codegen .inc files from field_registry.yaml..." + python "${REPODIR}"/cpp/src/grpc/codegen/generate_conversions.py \ + --registry "${REPODIR}"/cpp/src/grpc/codegen/field_registry.yaml \ + --output-dir "${REPODIR}"/cpp/src/grpc/codegen/generated + cp "${REPODIR}"/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto \ + "${REPODIR}"/cpp/src/grpc/cuopt_remote_data.proto + echo "Done. Remember to commit the generated files." +fi + ################################################################################ # Configure and build libcuopt (and optionally just the gRPC server) if buildAll || hasArg libcuopt || hasArg cuopt_grpc_server; then diff --git a/ci/test_cpp.sh b/ci/test_cpp.sh index e104604f3d..29576b3dfb 100755 --- a/ci/test_cpp.sh +++ b/ci/test_cpp.sh @@ -31,6 +31,9 @@ mkdir -p "${RAPIDS_TESTS_DIR}" rapids-print-env +rapids-logger "Verify codegen output matches committed files" +./ci/verify_codegen.sh + rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/verify_codegen.sh b/ci/verify_codegen.sh new file mode 100755 index 0000000000..c2ddf63bbf --- /dev/null +++ b/ci/verify_codegen.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Verify that committed codegen output matches what generate_conversions.py produces. +# Fails if a developer edited field_registry.yaml without re-running ./build.sh codegen. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +CODEGEN_DIR="${REPO_DIR}/cpp/src/grpc/codegen" +GENERATED_DIR="${CODEGEN_DIR}/generated" +PROTO_DEST="${REPO_DIR}/cpp/src/grpc/cuopt_remote_data.proto" + +TMPDIR=$(mktemp -d) +trap 'rm -rf ${TMPDIR}' EXIT + +echo "Running code generator into temp directory..." +python "${CODEGEN_DIR}/generate_conversions.py" \ + --registry "${CODEGEN_DIR}/field_registry.yaml" \ + --output-dir "${TMPDIR}" + +echo "Comparing generated output with committed files..." + +FAILED=0 + +for f in "${TMPDIR}"/*; do + fname=$(basename "$f") + committed="${GENERATED_DIR}/${fname}" + if [ ! -f "${committed}" ]; then + echo "MISSING: ${committed} (new generated file not committed)" + FAILED=1 + continue + fi + if ! diff -q "$f" "${committed}" > /dev/null 2>&1; then + echo "MISMATCH: cpp/src/grpc/codegen/generated/${fname}" + diff -u "${committed}" "$f" | head -30 + FAILED=1 + fi +done + +if [ -f "${TMPDIR}/cuopt_remote_data.proto" ] && [ -f "${PROTO_DEST}" ]; then + if ! diff -q "${TMPDIR}/cuopt_remote_data.proto" "${PROTO_DEST}" > /dev/null 2>&1; then + echo "MISMATCH: cpp/src/grpc/cuopt_remote_data.proto (not copied from codegen/generated)" + FAILED=1 + fi +fi + +if [ ${FAILED} -ne 0 ]; then + echo "" + echo "ERROR: Committed generated files are out of sync with field_registry.yaml." + echo "Run './build.sh codegen' and commit the results." + exit 1 +fi + +echo "OK: All generated files match field_registry.yaml." diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 17fb833834..7aee56978c 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -59,12 +59,14 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 +- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests - rmm==26.6.*,>=0.0.0a0 +- ruamel.yaml>=0.18 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index ff07cb5fce..537100f10c 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -59,12 +59,14 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 +- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests - rmm==26.6.*,>=0.0.0a0 +- ruamel.yaml>=0.18 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index c6d7cec52d..10009108b9 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -59,12 +59,14 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 +- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests - rmm==26.6.*,>=0.0.0a0 +- ruamel.yaml>=0.18 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index ab1d06a645..9c4b7308ad 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -59,12 +59,14 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 +- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests - rmm==26.6.*,>=0.0.0a0 +- ruamel.yaml>=0.18 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 682f9d33ef..93447c1924 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -74,6 +74,8 @@ cache: - make - ninja - git + - python + - pyyaml - tbb-devel - zlib - bzip2 diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index a24d14e266..917649c34f 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -339,8 +339,28 @@ if (NOT SKIP_GRPC_BUILD) endif () endif () - # Generate C++ code from cuopt_remote.proto (base message definitions) - set(PROTO_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/cuopt_remote.proto") + # Proto search paths: manual protos in src/grpc, generated data proto in src/grpc/codegen/generated + set(PROTO_PATH_MANUAL "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc") + set(PROTO_PATH_GEN "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated") + + # Generate C++ code from cuopt_remote_data.proto (auto-generated data definitions) + set(DATA_PROTO_FILE "${PROTO_PATH_GEN}/cuopt_remote_data.proto") + set(DATA_PROTO_SRCS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote_data.pb.cc") + set(DATA_PROTO_HDRS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote_data.pb.h") + + add_custom_command( + OUTPUT "${DATA_PROTO_SRCS}" "${DATA_PROTO_HDRS}" + COMMAND ${_PROTOBUF_PROTOC} + ARGS --cpp_out ${CMAKE_CURRENT_BINARY_DIR} + --proto_path ${PROTO_PATH_GEN} + ${DATA_PROTO_FILE} + DEPENDS ${DATA_PROTO_FILE} + COMMENT "Generating C++ code from cuopt_remote_data.proto" + VERBATIM + ) + + # Generate C++ code from cuopt_remote.proto (control/protocol messages, imports data proto) + set(PROTO_FILE "${PROTO_PATH_MANUAL}/cuopt_remote.proto") set(PROTO_SRCS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote.pb.cc") set(PROTO_HDRS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote.pb.h") @@ -348,15 +368,16 @@ if (NOT SKIP_GRPC_BUILD) OUTPUT "${PROTO_SRCS}" "${PROTO_HDRS}" COMMAND ${_PROTOBUF_PROTOC} ARGS --cpp_out ${CMAKE_CURRENT_BINARY_DIR} - --proto_path ${CMAKE_CURRENT_SOURCE_DIR}/src/grpc + --proto_path ${PROTO_PATH_MANUAL} + --proto_path ${PROTO_PATH_GEN} ${PROTO_FILE} - DEPENDS ${PROTO_FILE} + DEPENDS ${PROTO_FILE} ${DATA_PROTO_FILE} COMMENT "Generating C++ code from cuopt_remote.proto" VERBATIM ) # Generate gRPC service code from cuopt_remote_service.proto - set(GRPC_PROTO_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/cuopt_remote_service.proto") + set(GRPC_PROTO_FILE "${PROTO_PATH_MANUAL}/cuopt_remote_service.proto") set(GRPC_PROTO_SRCS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote_service.pb.cc") set(GRPC_PROTO_HDRS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote_service.pb.h") set(GRPC_SERVICE_SRCS "${CMAKE_CURRENT_BINARY_DIR}/cuopt_remote_service.grpc.pb.cc") @@ -368,9 +389,10 @@ if (NOT SKIP_GRPC_BUILD) ARGS --cpp_out ${CMAKE_CURRENT_BINARY_DIR} --grpc_out ${CMAKE_CURRENT_BINARY_DIR} --plugin=protoc-gen-grpc=${_GRPC_CPP_PLUGIN_EXECUTABLE} - --proto_path ${CMAKE_CURRENT_SOURCE_DIR}/src/grpc + --proto_path ${PROTO_PATH_MANUAL} + --proto_path ${PROTO_PATH_GEN} ${GRPC_PROTO_FILE} - DEPENDS ${GRPC_PROTO_FILE} ${PROTO_FILE} ${PROTO_SRCS} ${PROTO_HDRS} + DEPENDS ${GRPC_PROTO_FILE} ${PROTO_FILE} ${DATA_PROTO_FILE} COMMENT "Generating gRPC C++ code from cuopt_remote_service.proto" VERBATIM ) @@ -402,6 +424,7 @@ endif () if (NOT SKIP_GRPC_BUILD) # Add gRPC mapper files and generated protobuf sources set(GRPC_INFRA_FILES + ${DATA_PROTO_SRCS} ${PROTO_SRCS} ${GRPC_PROTO_SRCS} ${GRPC_SERVICE_SRCS} @@ -480,6 +503,7 @@ target_include_directories(cuopt "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc" "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/client" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated" "${CMAKE_CURRENT_BINARY_DIR}" "${CUDSS_INCLUDE}" PUBLIC @@ -810,6 +834,7 @@ if (NOT SKIP_GRPC_BUILD) "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc" "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/server" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated" "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/libmps_parser/include" "${CMAKE_CURRENT_BINARY_DIR}" diff --git a/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp b/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp index 009a8ce84e..00f23748b4 100644 --- a/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp +++ b/cpp/include/cuopt/linear_programming/cpu_optimization_problem.hpp @@ -111,6 +111,18 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t& get_quadratic_objective_offsets() const override; const std::vector& get_quadratic_objective_indices() const override; const std::vector& get_quadratic_objective_values() const override; + const std::vector& get_quadratic_objective_offsets_host() const + { + return get_quadratic_objective_offsets(); + } + const std::vector& get_quadratic_objective_indices_host() const + { + return get_quadratic_objective_indices(); + } + const std::vector& get_quadratic_objective_values_host() const + { + return get_quadratic_objective_values(); + } bool has_quadratic_objective() const override; // Host getters - these are the only supported getters for CPU implementation diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml new file mode 100644 index 0000000000..104683c3b8 --- /dev/null +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -0,0 +1,679 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Field registry for auto-generating proto definitions and C++ conversion code. +# +# Convention-over-configuration: +# - Bare field name = double type, getter = get_() +# - "field: type" = specified type +# - "field: { type: T, ... }" = full override +# +# Scalar getters default to get_(). +# Array getters default to get__host() (for optimization_problem) or +# get__host() for solution arrays. +# Proto field name = registry field name (always). +# Enum conventions: see the "Enums" section below for full defaults. +# +# Field numbers: +# field_num – proto message field number (auto-assigned if missing) +# array_id – enum value for ArrayFieldId or ResultFieldId (auto-assigned) +# +# Numbering is contiguous within each proto message (no artificial caps). +# The only hard ranges are for solution scalars that share ChunkedResultHeader: +# LP: 1000-1999 +# MIP: 2000-2999 +# WS: 3000-3999 +# +# Attributes (all optional): +# +# Per-field: +# type – proto wire type (default: double for scalars, repeated double for arrays) +# field_num – proto message field number (auto-assigned if missing) +# array_id – enum value for ArrayFieldId or ResultFieldId (auto-assigned) +# setter_getter_root – C++ getter/setter root when different from field name +# getter – explicit C++ getter expression (overrides setter_getter_root) +# setter – explicit C++ setter name +# member – C++ member name or access path (when different from field name) +# setter_group – name of multi-argument setter group +# conditional – true => guard serialization with emptiness check +# skip_conversion – true => include in proto but not in conversion code +# proto_only – true => include in proto header but not a constructor arg +# sentinel – sentinel value handling for fields with "unset" semantics +# to_proto_cast – explicit cast type for C++ -> proto direction +# from_proto_cast – explicit cast type for proto -> C++ direction +# +# Per-section: +# cpp_type – C++ type this section maps to +# constructor_args – mapping of constructor parameter names to field sources +# presence_check – predicate expression to test if optional data is present +# getter – (per-section) expression to access the section's C++ data +# +# See GRPC_CODE_GENERATION.md for the full specification and examples. + +# ───────────────────────────────────────────────────────────────────────────── +# Enums +# ───────────────────────────────────────────────────────────────────────────── +# +# Convention-based defaults (override any field explicitly when needed): +# cpp_type — {key}_t +# proto_type — PascalCase from key (acronyms PDLP/MIP/LP/QP/VRP/PDP uppercased) +# to_proto_fn — to_proto_{key}() +# from_proto — from_proto_{key}() +# default — first value (proto3 zero-value) +# values — bare names auto-number from 0; {Name: N} resets counter to N +# proto_prefix — "" (no prefix); set when proto values need disambiguation +# +# Proto value names: proto_prefix + UPPER_SNAKE(CppName), or just CppName if +# no prefix. +# +enums: + pdlp_termination_status: + domain: solution + proto_prefix: PDLP + values: + - NoTermination + - NumericalError + - Optimal + - PrimalInfeasible + - DualInfeasible + - IterationLimit + - TimeLimit + - ConcurrentLimit + - PrimalFeasible + + mip_termination_status: + domain: solution + proto_prefix: MIP + values: + - NoTermination + - Optimal + - FeasibleFound + - Infeasible + - Unbounded + - TimeLimit + - WorkLimit + + pdlp_solver_mode: + domain: settings + default: Stable3 + values: + - Stable1 + - Stable2 + - Methodical1 + - Fast1 + - Stable3 + + lp_method: + domain: settings + cpp_type: method_t + values: + - Concurrent + - PDLP + - DualSimplex + - Barrier + + variable_type: + domain: problem + cpp_type: var_t + values: + - CONTINUOUS + - INTEGER + + problem_category: + domain: problem + values: + - LP + - MIP + + # ResultFieldId is auto-derived from solution/warm-start array_id fields. + # Aliases here preserve backward compatibility with upstream's abbreviated + # rapids-pre-commit-hooks: disable-next-line + # names (26.04) while the codegen uses full, consistent canonical names. + result_field_id: + aliases: + RESULT_WS_CURRENT_PRIMAL: 3 + RESULT_WS_CURRENT_DUAL: 4 + RESULT_WS_INITIAL_PRIMAL_AVG: 5 + RESULT_WS_INITIAL_DUAL_AVG: 6 + RESULT_WS_SUM_PRIMAL: 8 + RESULT_WS_SUM_DUAL: 9 + RESULT_WS_LAST_RESTART_GAP_PRIMAL: 10 + RESULT_WS_LAST_RESTART_GAP_DUAL: 11 + +# ───────────────────────────────────────────────────────────────────────────── +# LP Solution +# ───────────────────────────────────────────────────────────────────────────── +lp_solution: + cpp_type: "cpu_lp_solution_t" + + scalars: + - lp_termination_status: + field_num: 1000 + type: pdlp_termination_status + getter: get_termination_status() + - error_message: + field_num: 1001 + type: string + getter: "get_error_status().what()" + proto_only: true + - l2_primal_residual: + field_num: 1002 + - l2_dual_residual: + field_num: 1003 + - primal_objective: + field_num: 1004 + getter: get_objective_value() + - dual_objective: + field_num: 1005 + getter: get_dual_objective_value() + - gap: + field_num: 1006 + - nb_iterations: + field_num: 1007 + type: int32 + getter: get_num_iterations() + - solve_time: + field_num: 1008 + - solved_by: + field_num: 1009 + type: int32 + getter: solved_by() + to_proto_cast: int32_t + from_proto_cast: method_t + + arrays: + - primal_solution: + field_num: 1 + array_id: 0 + - dual_solution: + field_num: 2 + array_id: 1 + - reduced_cost: + field_num: 3 + array_id: 2 + + constructor_args: + scalars: + - lp_termination_status + - primal_objective + - dual_objective + - solve_time + - l2_primal_residual + - l2_dual_residual + - gap + - nb_iterations + - solved_by + + warm_start: + presence_check: has_warm_start_data() + getter: get_cpu_pdlp_warm_start_data() + result_id_prefix: WS + chunked_header_prefix: ws_ + + scalars: + - initial_primal_weight: + field_num: 3000 + member: initial_primal_weight_ + - initial_step_size: + field_num: 3001 + member: initial_step_size_ + - total_pdlp_iterations: + field_num: 3002 + type: int32 + member: total_pdlp_iterations_ + - total_pdhg_iterations: + field_num: 3003 + type: int32 + member: total_pdhg_iterations_ + - last_candidate_kkt_score: + field_num: 3004 + member: last_candidate_kkt_score_ + - last_restart_kkt_score: + field_num: 3005 + member: last_restart_kkt_score_ + - sum_solution_weight: + field_num: 3006 + member: sum_solution_weight_ + - iterations_since_last_restart: + field_num: 3007 + type: int32 + member: iterations_since_last_restart_ + + arrays: + - current_primal_solution: + field_num: 1 + array_id: 3 + member: current_primal_solution_ + - current_dual_solution: + field_num: 2 + array_id: 4 + member: current_dual_solution_ + - initial_primal_average: + field_num: 3 + array_id: 5 + member: initial_primal_average_ + - initial_dual_average: + field_num: 4 + array_id: 6 + member: initial_dual_average_ + - current_ATY: + field_num: 5 + array_id: 7 + member: current_ATY_ + - sum_primal_solutions: + field_num: 6 + array_id: 8 + member: sum_primal_solutions_ + - sum_dual_solutions: + field_num: 7 + array_id: 9 + member: sum_dual_solutions_ + - last_restart_duality_gap_primal_solution: + field_num: 8 + array_id: 10 + member: last_restart_duality_gap_primal_solution_ + - last_restart_duality_gap_dual_solution: + field_num: 9 + array_id: 11 + member: last_restart_duality_gap_dual_solution_ + +# ───────────────────────────────────────────────────────────────────────────── +# MIP Solution +# ───────────────────────────────────────────────────────────────────────────── +mip_solution: + cpp_type: "cpu_mip_solution_t" + + scalars: + - mip_termination_status: + field_num: 2000 + type: mip_termination_status + getter: get_termination_status() + - mip_error_message: + field_num: 2001 + type: string + getter: "get_error_status().what()" + proto_only: true + - mip_objective: + field_num: 2002 + getter: get_objective_value() + - mip_gap: + field_num: 2003 + - solution_bound: + field_num: 2004 + - total_solve_time: + field_num: 2005 + getter: get_solve_time() + - presolve_time: + field_num: 2006 + - max_constraint_violation: + field_num: 2007 + - max_int_violation: + field_num: 2008 + - max_variable_bound_violation: + field_num: 2009 + - nodes: + field_num: 2010 + type: int32 + getter: get_num_nodes() + - simplex_iterations: + field_num: 2011 + type: int32 + getter: get_num_simplex_iterations() + + arrays: + - mip_solution: + field_num: 1 + array_id: 12 + getter: get_solution_host() + + constructor_args: + scalars: + - mip_termination_status + - mip_objective + - mip_gap + - solution_bound + - total_solve_time + - presolve_time + - max_constraint_violation + - max_int_violation + - max_variable_bound_violation + - nodes + - simplex_iterations + +# ───────────────────────────────────────────────────────────────────────────── +# PDLP Solver Settings +# ───────────────────────────────────────────────────────────────────────────── +# +# Settings use nested YAML to mirror C++ struct hierarchy. +# A list-valued entry is a sub-struct group (e.g. tolerances). +# +pdlp_settings: + cpp_type: "pdlp_solver_settings_t" + proto_type: "cuopt::remote::PDLPSolverSettings" + + fields: + # Termination tolerances (nested: settings.tolerances.) + - tolerances: + - absolute_gap_tolerance: + field_num: 1 + - relative_gap_tolerance: + field_num: 2 + - primal_infeasible_tolerance: + field_num: 3 + - dual_infeasible_tolerance: + field_num: 4 + - absolute_dual_tolerance: + field_num: 5 + - relative_dual_tolerance: + field_num: 6 + - absolute_primal_tolerance: + field_num: 7 + - relative_primal_tolerance: + field_num: 8 + + # Limits + - time_limit: + field_num: 9 + - iteration_limit: + field_num: 10 + type: int64 + sentinel: + to_proto: "std::numeric_limits::max()" + proto_value: -1 + from_proto_guard: ">= 0" + from_proto_cast: "i_t" + + # Solver configuration + - log_to_console: + field_num: 11 + type: bool + - detect_infeasibility: + field_num: 12 + type: bool + - strict_infeasibility: + field_num: 13 + type: bool + - pdlp_solver_mode: + field_num: 14 + type: pdlp_solver_mode + - method: + field_num: 15 + type: lp_method + - presolver: + field_num: 16 + type: int32 + to_proto_cast: "int32_t" + from_proto_cast: "presolver_t" + - dual_postsolve: + field_num: 17 + type: bool + - crossover: + field_num: 18 + type: bool + - num_gpus: + field_num: 19 + type: int32 + + - per_constraint_residual: + field_num: 20 + type: bool + - cudss_deterministic: + field_num: 21 + type: bool + - folding: + field_num: 22 + type: int32 + - augmented: + field_num: 23 + type: int32 + - dualize: + field_num: 24 + type: int32 + - ordering: + field_num: 25 + type: int32 + - barrier_dual_initial_point: + field_num: 26 + type: int32 + - eliminate_dense_columns: + field_num: 27 + type: bool + - save_best_primal_so_far: + field_num: 28 + type: bool + - first_primal_feasible: + field_num: 29 + type: bool + - pdlp_precision: + field_num: 30 + type: int32 + to_proto_cast: "int32_t" + from_proto_cast: "pdlp_precision_t" + +# ───────────────────────────────────────────────────────────────────────────── +# MIP Solver Settings +# ───────────────────────────────────────────────────────────────────────────── +mip_settings: + cpp_type: "mip_solver_settings_t" + proto_type: "cuopt::remote::MIPSolverSettings" + + fields: + # Limits + - time_limit: + field_num: 1 + + # Tolerances (nested: settings.tolerances.) + - tolerances: + - relative_mip_gap: + field_num: 2 + - absolute_mip_gap: + field_num: 3 + - integrality_tolerance: + field_num: 4 + - absolute_tolerance: + field_num: 5 + - relative_tolerance: + field_num: 6 + - presolve_absolute_tolerance: + field_num: 7 + + # Solver configuration + - log_to_console: + field_num: 8 + type: bool + - heuristics_only: + field_num: 9 + type: bool + - num_cpu_threads: + field_num: 10 + type: int32 + - num_gpus: + field_num: 11 + type: int32 + - presolver: + field_num: 12 + type: int32 + to_proto_cast: "int32_t" + from_proto_cast: "presolver_t" + - mip_scaling: + field_num: 13 + type: int32 + + # Additional limits + - work_limit: + field_num: 14 + - node_limit: + field_num: 15 + type: int32 + sentinel: + to_proto: "std::numeric_limits::max()" + proto_value: -1 + from_proto_guard: ">= 0" + from_proto_cast: "i_t" + + # Branching + - reliability_branching: + field_num: 16 + type: int32 + - mip_batch_pdlp_strong_branching: + field_num: 17 + type: int32 + - mip_batch_pdlp_reliability_branching: + field_num: 29 + type: int32 + - strong_branching_simplex_iteration_limit: + field_num: 30 + type: int32 + + # Cut configuration + - max_cut_passes: + field_num: 18 + type: int32 + - mir_cuts: + field_num: 19 + type: int32 + - mixed_integer_gomory_cuts: + field_num: 20 + type: int32 + - knapsack_cuts: + field_num: 21 + type: int32 + - clique_cuts: + field_num: 22 + type: int32 + - implied_bound_cuts: + field_num: 31 + type: int32 + - strong_chvatal_gomory_cuts: + field_num: 23 + type: int32 + - reduced_cost_strengthening: + field_num: 24 + type: int32 + - cut_change_threshold: + field_num: 25 + - cut_min_orthogonality: + field_num: 26 + + # Determinism and reproducibility + - determinism_mode: + field_num: 27 + type: int32 + - seed: + field_num: 28 + type: int32 + +# ───────────────────────────────────────────────────────────────────────────── +# Optimization Problem (cpu_optimization_problem_t) +# ───────────────────────────────────────────────────────────────────────────── +optimization_problem: + cpp_type: "cpu_optimization_problem_t" + proto_message: OptimizationProblem + + scalars: + - problem_name: + field_num: 1 + type: string + - objective_name: + field_num: 2 + type: string + - maximize: + field_num: 3 + type: bool + getter: get_sense() + - objective_scaling_factor: + field_num: 4 + - objective_offset: + field_num: 5 + + arrays: + - variable_names: + field_num: 7 + array_id: 0 + type: repeated string + - row_names: + field_num: 8 + array_id: 1 + type: repeated string + - A_values: + field_num: 9 + array_id: 2 + setter_getter_root: constraint_matrix_values + setter_group: csr_constraint_matrix + - A_indices: + field_num: 10 + array_id: 3 + setter_getter_root: constraint_matrix_indices + type: repeated int32 + setter_group: csr_constraint_matrix + - A_offsets: + field_num: 11 + array_id: 4 + setter_getter_root: constraint_matrix_offsets + type: repeated int32 + setter_group: csr_constraint_matrix + - c: + field_num: 12 + array_id: 5 + setter_getter_root: objective_coefficients + - b: + field_num: 13 + array_id: 6 + setter_getter_root: constraint_bounds + conditional: true + - variable_lower_bounds: + field_num: 14 + array_id: 7 + - variable_upper_bounds: + field_num: 15 + array_id: 8 + - constraint_lower_bounds: + field_num: 16 + array_id: 9 + conditional: true + - constraint_upper_bounds: + field_num: 17 + array_id: 10 + conditional: true + - row_types: + field_num: 18 + array_id: 11 + type: bytes + conditional: true + - variable_types: + field_num: 19 + array_id: 12 + type: repeated variable_type + - initial_primal_solution: + field_num: 20 + array_id: 13 + skip_conversion: true + - initial_dual_solution: + field_num: 21 + array_id: 14 + skip_conversion: true + - Q_values: + field_num: 22 + array_id: 15 + setter_getter_root: quadratic_objective_values + setter_group: quadratic_objective + - Q_indices: + field_num: 23 + array_id: 16 + setter_getter_root: quadratic_objective_indices + type: repeated int32 + setter_group: quadratic_objective + - Q_offsets: + field_num: 24 + array_id: 17 + setter_getter_root: quadratic_objective_offsets + type: repeated int32 + setter_group: quadratic_objective + + setter_groups: + csr_constraint_matrix: + setter: set_csr_constraint_matrix + fields: [A_values, A_indices, A_offsets] + quadratic_objective: + setter: set_quadratic_objective_matrix + fields: [Q_values, Q_indices, Q_offsets] diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py new file mode 100644 index 0000000000..a0401a4c1a --- /dev/null +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -0,0 +1,2504 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Code generator for proto definitions, enum converters, and settings, problem, solution conversions. + +Reads field_registry.yaml and generates: + 1. cuopt_remote_data.proto — full proto: enums, OptimizationProblem, settings, solutions, + ChunkedResultHeader + 2. Enum converter C++ switch functions (.inc) + 3. Settings conversion C++ function bodies (.inc) + 4. Solution conversion C++ function bodies (.inc) — all 8 solution mapper functions + 5. Problem conversion C++ function bodies (.inc) + +Usage: + python generate_conversions.py [--registry field_registry.yaml] [--output-dir .] [--auto-number] +""" + +import argparse +import os +import re +import sys +from pathlib import Path + +import yaml + +# ============================================================================ +# Utilities +# ============================================================================ + +HEADER = """\ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ +""" + + +def write_file(path, content): + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) + with open(path, "w") as f: + f.write(content) + print(f" wrote {path}") + + +def parse_field(entry): + """Parse a field entry from the YAML, which may be a bare string or a dict. + + Does NOT set a default 'type' — callers supply context-appropriate defaults + (e.g. "double" for scalars, "repeated double" for arrays). + """ + if isinstance(entry, str): + return {"name": entry} + if isinstance(entry, dict): + assert len(entry) == 1, f"Expected single-key dict, got {entry}" + name = list(entry.keys())[0] + val = entry[name] + if isinstance(val, str): + return {"name": name, "type": val} + if isinstance(val, dict): + result = {"name": name} + result.update(val) + return result + if val is None: + return {"name": name} + raise ValueError(f"Unexpected field format: {entry}") + + +def parse_settings_fields(entries, prefix=""): + """Parse settings fields, handling nested sub-structs (e.g. tolerances).""" + for entry in entries: + assert isinstance(entry, dict) and len(entry) == 1 + name = list(entry.keys())[0] + val = entry[name] + if isinstance(val, list): + yield from parse_settings_fields(val, prefix=f"{prefix}{name}.") + else: + f = parse_field(entry) + if prefix or "member" not in f: + f["member"] = f"{prefix}{name}" if prefix else name + yield f + + +def parse_enum_entry(entry, index=0): + """Parse a single enum value entry. + + Supports three forms: + - bare string 'CppName' → (name, index) + - {CppName: null} → (name, index) + - {CppName: number} → (name, number) + + The caller is responsible for tracking the running counter; explicit values + reset it (C-style enum semantics). + """ + if isinstance(entry, str): + return entry, index + assert isinstance(entry, dict) and len(entry) == 1 + name = list(entry.keys())[0] + num = entry[name] + return name, num if num is not None else index + + +def parse_enum_values(values): + """Parse a full enum values list with C-style auto-numbering. + + Bare names get the next sequential number; explicit {Name: N} overrides + reset the counter so the next bare name gets N+1. + """ + counter = 0 + result = [] + for entry in values: + name, num = parse_enum_entry(entry, index=counter) + result.append((name, num)) + counter = num + 1 + return result + + +def camel_to_upper_snake(name): + """Convert CamelCase to UPPER_SNAKE_CASE: PrimalInfeasible -> PRIMAL_INFEASIBLE.""" + s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s) + return s.upper() + + +def proto_type(t): + """Map YAML type to proto type.""" + return { + "double": "double", + "float": "float", + "int32": "int32", + "int64": "int64", + "bool": "bool", + "string": "string", + "bytes": "bytes", + }.get(t, t) + + +def _from_proto_cast(ftype): + """Return C++ cast for reading a proto field back into template type.""" + if ftype in ("double", "float"): + return "f_t" + if ftype in ("int32", "int64"): + return "i_t" + return None + + +def _array_element_cast(ftype): + """Cast for serializing array elements to proto (to_proto direction).""" + if "double" in ftype: + return "double" + if "int32" in ftype: + return "int32_t" + return None + + +def _array_cpp_type(ftype): + """C++ vector element type for proto-to-C++ direction.""" + if "double" in ftype: + return "f_t" + if "int32" in ftype: + return "i_t" + return None + + +def _default_element_size(f): + """Default element byte size for a field.""" + if "element_size" in f: + return f["element_size"] + ftype = f.get("type", "repeated double") + if "int32" in ftype: + return 4 + if ftype in ("bytes", "repeated string"): + return 1 + if ftype.startswith("repeated ") and ftype.split()[-1] not in ( + "double", + "int32", + "string", + ): + return 4 + return 8 + + +# ============================================================================ +# Enum helpers — convention-based derivation +# ============================================================================ + +KNOWN_ACRONYMS = {"pdlp", "mip", "lp", "qp", "vrp", "pdp", "tsp"} + + +def _snake_to_pascal(key): + """Convert snake_case key to PascalCase, uppercasing known acronyms. + + pdlp_termination_status -> PDLPTerminationStatus + lp_method -> LPMethod + variable_type -> VariableType + """ + return "".join( + part.upper() if part in KNOWN_ACRONYMS else part.capitalize() + for part in key.split("_") + ) + + +def _enum_proto_type(key, edef): + """Derive proto enum type name: explicit or PascalCase from key.""" + return edef.get("proto_type", _snake_to_pascal(key)) + + +def _enum_cpp_type(key, edef): + """Derive C++ type: explicit or {key}_t.""" + return edef.get("cpp_type", f"{key}_t") + + +def _enum_default(key, edef): + """Derive default value: explicit or first value name (proto3 zero-value).""" + if "default" in edef: + return edef["default"] + first = edef["values"][0] + name, _ = parse_enum_entry(first) + return name + + +def _enum_to_proto_fn(key, _edef): + """Derive to_proto function name: to_proto_{key}.""" + return f"to_proto_{key}" + + +def _enum_from_proto_fn(key, _edef): + """Derive from_proto function name: from_proto_{key}.""" + return f"from_proto_{key}" + + +def _proto_enum_value_name(cpp_name, prefix): + """Derive proto enum value name from C++ name and prefix.""" + if not prefix: + return cpp_name + return f"{prefix}_{camel_to_upper_snake(cpp_name)}" + + +def _lookup_enum(registry, type_name): + """Find an enum definition in the registry by key name.""" + return registry.get("enums", {}).get(type_name) + + +def _is_enum_type(registry, type_name): + """Check if a type name refers to a registered enum.""" + return type_name in registry.get("enums", {}) + + +def _is_repeated_enum(registry, ftype): + """Check if a type is 'repeated ' and return enum key if so.""" + if ftype.startswith("repeated "): + inner = ftype.split(None, 1)[1] + if _is_enum_type(registry, inner): + return inner + return None + + +def _proto_cpp_name(name): + """Protobuf lowercases all field names for C++ accessor methods.""" + return name.lower() + + +# ============================================================================ +# Problem field helpers +# ============================================================================ + + +def _problem_getter_root(f): + """Get the getter root for a problem field (setter_getter_root or name).""" + return f.get("setter_getter_root", f["name"]) + + +def _default_problem_getter(f, is_scalar): + """Default getter for an optimization_problem field.""" + if "getter" in f: + return f["getter"] + root = _problem_getter_root(f) + if is_scalar: + return f"get_{root}()" + ftype = f.get("type", "repeated double") + if ftype == "repeated string": + return f"get_{root}()" + return f"get_{root}_host()" + + +def _default_problem_setter(f): + """Default setter name for an optimization_problem field.""" + if "setter" in f: + return f["setter"] + root = _problem_getter_root(f) + return f"set_{root}" + + +def _find_problem_field(obj, name): + """Find a field by name in the optimization_problem arrays list.""" + for entry in obj.get("arrays", []): + f = parse_field(entry) + if f["name"] == name: + return f + return None + + +def _field_array_id_name(f): + """Derive ArrayFieldId enum name from field name: FIELD_{UPPER_SNAKE(name)}.""" + return f"FIELD_{f['name'].upper()}" + + +def _field_result_id_name(f, prefix=""): + """Derive ResultFieldId enum name from field name: RESULT_{PREFIX_}{UPPER_SNAKE(name)}.""" + if prefix: + return f"RESULT_{prefix}_{f['name'].upper()}" + return f"RESULT_{f['name'].upper()}" + + +def _problem_field_proto_type(registry, ftype): + """Map optimization_problem field type to proto type.""" + if not ftype: + return "double" + enum_key = _is_repeated_enum(registry, ftype) + if enum_key: + edef = _lookup_enum(registry, enum_key) + return f"repeated {_enum_proto_type(enum_key, edef)}" + if ftype in ( + "repeated double", + "repeated int32", + "repeated string", + "bytes", + ): + return ftype + edef = _lookup_enum(registry, ftype) + if edef: + return _enum_proto_type(ftype, edef) + return proto_type(ftype) + + +def _settings_field_proto_type(registry, f): + """Map settings field type to proto type.""" + ftype = f.get("type", "double") + edef = _lookup_enum(registry, ftype) + if edef: + return _enum_proto_type(ftype, edef) + return proto_type(ftype) + + +def _solution_scalar_proto_type(registry, f): + """Map solution scalar type to proto type.""" + ftype = f.get("type", "double") + edef = _lookup_enum(registry, ftype) + if edef: + return _enum_proto_type(ftype, edef) + return proto_type(ftype) + + +# ============================================================================ +# Enum generation +# ============================================================================ + + +def generate_enum_proto_from_registry(registry): + """Generate all enum definitions for the proto file. + + Skips enum entries without 'values' (e.g. result_field_id which is + auto-derived and only carries aliases). + """ + enums = registry.get("enums", {}) + blocks = [] + for key, edef in enums.items(): + if "values" not in edef: + continue + proto_type_name = _enum_proto_type(key, edef) + prefix = edef.get("proto_prefix", "") + lines = [f"enum {proto_type_name} {{"] + for cpp_name, num in parse_enum_values(edef["values"]): + pname = _proto_enum_value_name(cpp_name, prefix) + lines.append(f" {pname} = {num};") + lines.append("}") + blocks.append("\n".join(lines)) + return "\n\n".join(blocks) + + +def generate_enum_converters_inc(registry, domain=None): + """Generate C++ to_proto/from_proto switch functions for enums. + + If domain is given, only emit converters for enums with that domain tag. + If domain is None, emit all converters (backward compat). + Skips enum entries without 'values' (auto-derived enums like result_field_id). + """ + enums = registry.get("enums", {}) + funcs = [] + for key, edef in enums.items(): + if "values" not in edef: + continue + if domain is not None and edef.get("domain") != domain: + continue + cpp_type = _enum_cpp_type(key, edef) + proto_type_name = _enum_proto_type(key, edef) + to_fn = _enum_to_proto_fn(key, edef) + from_fn = _enum_from_proto_fn(key, edef) + prefix = edef.get("proto_prefix", "") + default_cpp = _enum_default(key, edef) + default_proto = _proto_enum_value_name(default_cpp, prefix) + + # to_proto + lines = [ + f"cuopt::remote::{proto_type_name} {to_fn}({cpp_type} v)", + "{", + " switch (v) {", + ] + for cpp_name, _ in parse_enum_values(edef["values"]): + pname = _proto_enum_value_name(cpp_name, prefix) + lines.append( + f" case {cpp_type}::{cpp_name}: return cuopt::remote::{pname};" + ) + lines.append(f" default: return cuopt::remote::{default_proto};") + lines.extend([" }", "}"]) + funcs.append("\n".join(lines)) + + # from_proto + lines = [ + f"{cpp_type} {from_fn}(cuopt::remote::{proto_type_name} v)", + "{", + " switch (v) {", + ] + for cpp_name, _ in parse_enum_values(edef["values"]): + pname = _proto_enum_value_name(cpp_name, prefix) + lines.append( + f" case cuopt::remote::{pname}: return {cpp_type}::{cpp_name};" + ) + lines.append(f" default: return {cpp_type}::{default_cpp};") + lines.extend([" }", "}"]) + funcs.append("\n".join(lines)) + + return "\n\n".join(funcs) + + +# ============================================================================ +# Proto file generation — ResultFieldId, ArrayFieldId, messages +# ============================================================================ + + +def generate_proto_result_enums(registry): + """Generate ResultFieldId enum entries (sorted by array_id). + + Primary entries are auto-derived from solution/warm-start array_id fields. + Aliases come from enums.result_field_id.aliases in the registry, providing + backward-compatible names for existing clients. + """ + entries = [] + for obj_name in ["lp_solution", "mip_solution"]: + obj = registry.get(obj_name, {}) + for entry in obj.get("arrays", []): + f = parse_field(entry) + enum_id = _field_result_id_name(f) + aid = f.get("array_id") + if aid is not None: + entries.append((aid, f" {enum_id} = {aid};")) + ws = obj.get("warm_start", {}) + ws_rid_prefix = ws.get("result_id_prefix", "") + for entry in ws.get("arrays", []): + f = parse_field(entry) + enum_id = _field_result_id_name(f, prefix=ws_rid_prefix) + aid = f.get("array_id") + if aid is not None: + entries.append((aid, f" {enum_id} = {aid};")) + entries.sort(key=lambda x: x[0]) + seen, lines = set(), [] + for _, line in entries: + eid = line.split(" = ")[0].strip() + if eid not in seen: + lines.append(line) + seen.add(eid) + + rfid_enum = registry.get("enums", {}).get("result_field_id", {}) + alias_map = rfid_enum.get("aliases", {}) + if alias_map: + alias_lines = sorted( + (v, f" {k} = {v};") for k, v in alias_map.items() + ) + lines.insert(0, " option allow_alias = true;") + for _, line in alias_lines: + lines.append(line) + return "\n".join(lines) + + +def generate_array_field_id_enum(registry): + """Generate ArrayFieldId enum for the proto file.""" + obj = registry.get("optimization_problem", {}) + entries = [] + for entry in obj.get("arrays", []): + f = parse_field(entry) + afid = _field_array_id_name(f) + afnum = f.get("array_id") + if afnum is not None: + entries.append((afnum, f" {afid} = {afnum};")) + entries.sort(key=lambda x: x[0]) + return "\n".join(e[1] for e in entries) + + +def generate_array_field_element_size_inc(registry): + """Generate body of array_field_element_size() switch function.""" + obj = registry.get("optimization_problem", {}) + cases_by_size = {} + for entry in obj.get("arrays", []): + f = parse_field(entry) + afid = _field_array_id_name(f) + size = _default_element_size(f) + cases_by_size.setdefault(size, []).append(afid) + lines = [" switch (field_id) {"] + for size in sorted(cases_by_size.keys()): + if size == 8: + continue + for afid in cases_by_size[size]: + lines.append(f" case cuopt::remote::{afid}:") + lines.append(f" return {size};") + lines.append(" default: return 8;") + lines.append(" }") + return "\n".join(lines) + + +# ============================================================================ +# Settings proto + conversion generation +# ============================================================================ + + +def generate_settings_message_proto(registry, message_name, obj): + lines = [] + for f in parse_settings_fields(obj.get("fields", [])): + num = f.get("field_num") + if num is None: + continue + ptype = _settings_field_proto_type(registry, f) + lines.append((num, f" {ptype} {f['name']} = {num};")) + if message_name == "PDLPSolverSettings": + lines.append((50, " PDLPWarmStartData warm_start_data = 50;")) + lines.sort(key=lambda x: x[0]) + return "\n".join(item[1] for item in lines) + + +def generate_settings_to_proto_body(registry, obj_name, obj, indent=" "): + lines, ind = [], indent + for f in parse_settings_fields(obj.get("fields", [])): + name, ftype = f["name"], f.get("type", "double") + pname = _proto_cpp_name(name) + cpp_member = f.get("member", f["name"]) + sentinel = f.get("sentinel") + to_proto_cast = f.get("to_proto_cast") + + edef = _lookup_enum(registry, ftype) + to_fn = _enum_to_proto_fn(ftype, edef) if edef else None + + if sentinel: + sv, pv = sentinel["to_proto"], sentinel["proto_value"] + lines.append(f"{ind}if (settings.{cpp_member} == {sv}) {{") + lines.append(f"{ind} pb_settings->set_{pname}({pv});") + lines.append(f"{ind}}} else {{") + cast = to_proto_cast or ("int64_t" if ftype == "int64" else None) + expr = ( + f"static_cast<{cast}>(settings.{cpp_member})" + if cast + else f"settings.{cpp_member}" + ) + lines.append(f"{ind} pb_settings->set_{pname}({expr});") + lines.append(f"{ind}}}") + elif to_fn: + lines.append( + f"{ind}pb_settings->set_{pname}({to_fn}(settings.{cpp_member}));" + ) + elif to_proto_cast: + lines.append( + f"{ind}pb_settings->set_{pname}(static_cast<{to_proto_cast}>(settings.{cpp_member}));" + ) + else: + lines.append( + f"{ind}pb_settings->set_{pname}(settings.{cpp_member});" + ) + return "\n".join(lines) + + +def generate_proto_to_settings_body(registry, obj_name, obj, indent=" "): + lines, ind = [], indent + for f in parse_settings_fields(obj.get("fields", [])): + name, ftype = f["name"], f.get("type", "double") + pname = _proto_cpp_name(name) + cpp_member = f.get("member", f["name"]) + from_proto_cast = f.get("from_proto_cast") + sentinel = f.get("sentinel") + + edef = _lookup_enum(registry, ftype) + from_fn = _enum_from_proto_fn(ftype, edef) if edef else None + + if sentinel: + guard = sentinel["from_proto_guard"] + cast = sentinel.get("from_proto_cast", from_proto_cast) + lines.append(f"{ind}if (pb_settings.{pname}() {guard}) {{") + expr = ( + f"static_cast<{cast}>(pb_settings.{pname}())" + if cast + else f"pb_settings.{pname}()" + ) + lines.append(f"{ind} settings.{cpp_member} = {expr};") + lines.append(f"{ind}}}") + elif from_fn: + lines.append( + f"{ind}settings.{cpp_member} = {from_fn}(pb_settings.{pname}());" + ) + elif from_proto_cast: + lines.append( + f"{ind}settings.{cpp_member} = static_cast<{from_proto_cast}>(pb_settings.{pname}());" + ) + else: + lines.append( + f"{ind}settings.{cpp_member} = pb_settings.{pname}();" + ) + return "\n".join(lines) + + +# ============================================================================ +# Proto message generation — WarmStart, LPSolution, MIPSolution +# ============================================================================ + + +def generate_warm_start_message_proto(registry): + obj = registry.get("lp_solution", {}).get("warm_start", {}) + lines = [] + for entry in obj.get("arrays", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + lines.append((num, f" repeated double {f['name']} = {num};")) + for entry in obj.get("scalars", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + lines.append( + ( + num, + f" {proto_type(f.get('type', 'double'))} {f['name']} = {num};", + ) + ) + lines.sort(key=lambda x: x[0]) + return "\n".join(item[1] for item in lines) + + +def generate_lp_solution_message_proto(registry): + obj = registry.get("lp_solution", {}) + lines = [] + for entry in obj.get("arrays", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + lines.append((num, f" repeated double {f['name']} = {num};")) + lines.append((4, " PDLPWarmStartData warm_start_data = 4;")) + for entry in obj.get("scalars", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + ptype = _solution_scalar_proto_type(registry, f) + lines.append((num, f" {ptype} {f['name']} = {num};")) + lines.sort(key=lambda x: x[0]) + return "\n".join(item[1] for item in lines) + + +def generate_mip_solution_message_proto(registry): + obj = registry.get("mip_solution", {}) + lines = [] + for entry in obj.get("arrays", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + lines.append((num, f" repeated double {f['name']} = {num};")) + for entry in obj.get("scalars", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + ptype = _solution_scalar_proto_type(registry, f) + lines.append((num, f" {ptype} {f['name']} = {num};")) + lines.sort(key=lambda x: x[0]) + return "\n".join(item[1] for item in lines) + + +def generate_optimization_problem_proto(registry): + obj = registry.get("optimization_problem", {}) + if not obj: + return "" + lines = [] + for entry in obj.get("scalars", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + ptype = _problem_field_proto_type( + registry, f.get("type", "double") + ) + lines.append((num, f" {ptype} {f['name']} = {num};")) + for entry in obj.get("arrays", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + ptype = _problem_field_proto_type( + registry, f.get("type", "repeated double") + ) + lines.append((num, f" {ptype} {f['name']} = {num};")) + lines.sort(key=lambda x: x[0]) + return "\n".join(item[1] for item in lines) + + +def generate_chunked_result_header_proto(registry): + pc_enum = _enum_proto_type( + "problem_category", registry["enums"]["problem_category"] + ) + lines = [(1, f" {pc_enum} problem_category = 1;")] + for obj_name in ["lp_solution", "mip_solution"]: + obj = registry.get(obj_name, {}) + for entry in obj.get("scalars", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + ptype = _solution_scalar_proto_type(registry, f) + lines.append((num, f" {ptype} {f['name']} = {num};")) + ws = obj.get("warm_start", {}) + ws_ch_prefix = ws.get("chunked_header_prefix", "") + for entry in ws.get("scalars", []): + f = parse_field(entry) + num = f.get("field_num") + if num is not None: + ch_name = f"{ws_ch_prefix}{f['name']}" + lines.append( + ( + num, + f" {proto_type(f.get('type', 'double'))} {ch_name} = {num};", + ) + ) + lines.append((50, " repeated ResultArrayDescriptor arrays = 50;")) + lines.sort(key=lambda x: x[0]) + return "\n".join(item[1] for item in lines) + + +# ============================================================================ +# Full data proto assembly +# ============================================================================ + + +def generate_data_proto(registry): + enums = generate_enum_proto_from_registry(registry) + rfid = ( + "enum ResultFieldId {\n" + + generate_proto_result_enums(registry) + + "\n}" + ) + opt_prob = "" + if "optimization_problem" in registry: + opt_prob = ( + "message OptimizationProblem {\n" + + generate_optimization_problem_proto(registry) + + "\n}\n" + ) + pdlp_s = ( + "message PDLPSolverSettings {\n" + + generate_settings_message_proto( + registry, "PDLPSolverSettings", registry["pdlp_settings"] + ) + + "\n}" + ) + mip_s = ( + "message MIPSolverSettings {\n" + + generate_settings_message_proto( + registry, "MIPSolverSettings", registry["mip_settings"] + ) + + "\n}" + ) + ws = ( + "message PDLPWarmStartData {\n" + + generate_warm_start_message_proto(registry) + + "\n}" + ) + lp = ( + "message LPSolution {\n" + + generate_lp_solution_message_proto(registry) + + "\n}" + ) + mip = ( + "message MIPSolution {\n" + + generate_mip_solution_message_proto(registry) + + "\n}" + ) + rad = ( + "message ResultArrayDescriptor {\n" + " ResultFieldId field_id = 1;\n" + " int64 total_elements = 2;\n" + " int64 element_size_bytes = 3;\n" + "}" + ) + ch = ( + "message ChunkedResultHeader {\n" + + generate_chunked_result_header_proto(registry) + + "\n}" + ) + afid = "" + if "optimization_problem" in registry: + afid_body = generate_array_field_id_enum(registry) + if afid_body: + afid = "enum ArrayFieldId {\n" + afid_body + "\n}\n" + parts = [ + "// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml", + "// DO NOT EDIT — regenerate with: python cpp/src/grpc/codegen/generate_conversions.py", + "", + 'syntax = "proto3";', + "", + "package cuopt.remote;", + "", + enums, + "", + rfid, + "", + afid, + opt_prob, + pdlp_s, + "", + mip_s, + "", + ws, + "", + lp, + "", + mip, + "", + rad, + "", + ch, + "", + ] + return "\n".join(parts) + + +# ============================================================================ +# Solution conversion code generation +# ============================================================================ + + +def _gen_solution_to_proto(registry, obj_name, obj, indent=" "): + """Generate body of map_{lp,mip}_solution_to_proto().""" + ind = indent + lines = [] + + for entry in obj.get("scalars", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + getter = f.get("getter", f"get_{name}()") + to_cast = f.get("to_proto_cast") + edef = _lookup_enum(registry, f.get("type", "double")) + if edef: + to_fn = _enum_to_proto_fn(f["type"], edef) + lines.append( + f"{ind}pb_solution->set_{pname}({to_fn}(solution.{getter}));" + ) + elif to_cast: + lines.append( + f"{ind}pb_solution->set_{pname}(static_cast<{to_cast}>(solution.{getter}));" + ) + else: + lines.append(f"{ind}pb_solution->set_{pname}(solution.{getter});") + + lines.append("") + + for entry in obj.get("arrays", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + getter = f.get("getter", f"get_{f['name']}_host()") + var = f"_{f['name']}" + lines.append(f"{ind}const auto& {var} = solution.{getter};") + lines.append( + f"{ind}for (const auto& v : {var}) pb_solution->add_{pname}(static_cast(v));" + ) + + # Warm start + ws = obj.get("warm_start") + if ws: + check = ws.get("presence_check", "has_warm_start_data()") + ws_getter = ws.get("getter", "get_cpu_pdlp_warm_start_data()") + lines.append("") + lines.append(f"{ind}if (solution.{check}) {{") + lines.append( + f"{ind} auto* pb_ws = pb_solution->mutable_warm_start_data();" + ) + lines.append(f"{ind} const auto& ws = solution.{ws_getter};") + for entry in ws.get("arrays", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + member = f.get("member", f["name"]) + lines.append( + f"{ind} for (const auto& v : ws.{member}) pb_ws->add_{pname}(static_cast(v));" + ) + for entry in ws.get("scalars", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + member = f.get("member", f["name"]) + ftype = f.get("type", "double") + cast = ( + "double" + if ftype == "double" + else ("int32_t" if ftype == "int32" else "double") + ) + lines.append( + f"{ind} pb_ws->set_{pname}(static_cast<{cast}>(ws.{member}));" + ) + lines.append(f"{ind}}}") + + return "\n".join(lines) + + +def _gen_proto_to_solution(registry, obj_name, obj, indent=" "): + """Generate body of map_proto_to_{lp,mip}_solution().""" + ind = indent + lines = [] + cpp_type = obj["cpp_type"] + constructor_args = obj.get("constructor_args", {}) + ws = obj.get("warm_start") + + for entry in obj.get("arrays", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + lines.append( + f"{ind}std::vector {f['name']}(pb_solution.{pname}().begin(), pb_solution.{pname}().end());" + ) + + lines.append("") + + scalar_vars = {} + for entry in obj.get("scalars", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + if f.get("proto_only"): + continue + ftype = f.get("type", "double") + from_cast = f.get("from_proto_cast") + edef = _lookup_enum(registry, ftype) + if edef: + from_fn = _enum_from_proto_fn(ftype, edef) + lines.append( + f"{ind}auto _{name} = {from_fn}(pb_solution.{pname}());" + ) + elif from_cast: + lines.append( + f"{ind}auto _{name} = static_cast<{from_cast}>(pb_solution.{pname}());" + ) + else: + cast = _from_proto_cast(ftype) + if cast: + lines.append( + f"{ind}auto _{name} = static_cast<{cast}>(pb_solution.{pname}());" + ) + else: + lines.append(f"{ind}auto _{name} = pb_solution.{pname}();") + scalar_vars[name] = f"_{name}" + + array_names = [parse_field(e)["name"] for e in obj.get("arrays", [])] + arg_scalars = constructor_args.get("scalars", []) + args = [f"std::move({n})" for n in array_names] + args += [scalar_vars.get(s, f"_{s}") for s in arg_scalars] + + # Warm start + if ws: + accessor_getter = "warm_start_data" + lines.append("") + lines.append(f"{ind}if (pb_solution.has_{accessor_getter}()) {{") + lines.append( + f"{ind} const auto& pb_ws = pb_solution.{accessor_getter}();" + ) + lines.append(f"{ind} cpu_pdlp_warm_start_data_t ws;") + + for entry in ws.get("arrays", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + member = f.get("member", f["name"]) + lines.append( + f"{ind} ws.{member}.assign(pb_ws.{pname}().begin(), pb_ws.{pname}().end());" + ) + + for entry in ws.get("scalars", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + member = f.get("member", f["name"]) + ftype = f.get("type", "double") + cast = _from_proto_cast(ftype) + if cast: + lines.append( + f"{ind} ws.{member} = static_cast<{cast}>(pb_ws.{pname}());" + ) + else: + lines.append(f"{ind} ws.{member} = pb_ws.{pname}();") + + ws_args = args + ["std::move(ws)"] + lines.append(f"{ind} return {cpp_type}({', '.join(ws_args)});") + lines.append(f"{ind}}}") + lines.append("") + + lines.append(f"{ind}return {cpp_type}({', '.join(args)});") + return "\n".join(lines) + + +def _gen_chunked_header(registry, obj_name, obj, indent=" "): + """Generate body of populate_chunked_result_header_{lp,mip}().""" + ind = indent + lines = [] + category = "MIP" if obj_name == "mip_solution" else "LP" + lines.append( + f"{ind}header->set_problem_category(cuopt::remote::{category});" + ) + + for entry in obj.get("scalars", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + getter = f.get("getter", f"get_{name}()") + to_cast = f.get("to_proto_cast") + edef = _lookup_enum(registry, f.get("type", "double")) + if edef: + to_fn = _enum_to_proto_fn(f["type"], edef) + lines.append( + f"{ind}header->set_{pname}({to_fn}(solution.{getter}));" + ) + elif to_cast: + lines.append( + f"{ind}header->set_{pname}(static_cast<{to_cast}>(solution.{getter}));" + ) + else: + lines.append(f"{ind}header->set_{pname}(solution.{getter});") + + lines.append("") + + for entry in obj.get("arrays", []): + f = parse_field(entry) + getter = f.get("getter", f"get_{f['name']}_host()") + eid = _field_result_id_name(f) + lines.append( + f"{ind}add_result_array_descriptor(header, cuopt::remote::{eid}, solution.{getter}.size(), sizeof(double));" + ) + + # Warm start + ws = obj.get("warm_start") + if ws: + check = ws.get("presence_check", "has_warm_start_data()") + ws_getter = ws.get("getter", "get_cpu_pdlp_warm_start_data()") + ws_ch_prefix = ws.get("chunked_header_prefix", "") + ws_rid_prefix = ws.get("result_id_prefix", "") + lines.append("") + lines.append(f"{ind}if (solution.{check}) {{") + lines.append(f"{ind} const auto& ws = solution.{ws_getter};") + for entry in ws.get("scalars", []): + f = parse_field(entry) + ch_name = f"{ws_ch_prefix}{f['name']}" + pname = _proto_cpp_name(ch_name) + member = f.get("member", f["name"]) + ftype = f.get("type", "double") + cast = ( + "double" + if ftype == "double" + else ("int32_t" if ftype == "int32" else "double") + ) + lines.append( + f"{ind} header->set_{pname}(static_cast<{cast}>(ws.{member}));" + ) + lines.append("") + for entry in ws.get("arrays", []): + f = parse_field(entry) + member = f.get("member", f["name"]) + eid = _field_result_id_name(f, prefix=ws_rid_prefix) + lines.append( + f"{ind} add_result_array_descriptor(header, cuopt::remote::{eid}, ws.{member}.size(), sizeof(double));" + ) + lines.append(f"{ind}}}") + + return "\n".join(lines) + + +def _gen_collect_arrays(registry, obj_name, obj, indent=" "): + """Generate body of collect_{lp,mip}_solution_arrays().""" + ind = indent + lines = [f"{ind}std::map> arrays;"] + + for entry in obj.get("arrays", []): + f = parse_field(entry) + getter = f.get("getter", f"get_{f['name']}_host()") + eid = _field_result_id_name(f) + var = f"_{f['name']}" + lines.append(f"{ind}const auto& {var} = solution.{getter};") + lines.append( + f"{ind}if (!{var}.empty()) {{ arrays[cuopt::remote::{eid}] = doubles_to_bytes({var}); }}" + ) + + ws = obj.get("warm_start") + if ws: + check = ws.get("presence_check", "has_warm_start_data()") + ws_getter = ws.get("getter", "get_cpu_pdlp_warm_start_data()") + ws_rid_prefix = ws.get("result_id_prefix", "") + lines.append(f"{ind}if (solution.{check}) {{") + lines.append(f"{ind} const auto& ws = solution.{ws_getter};") + for entry in ws.get("arrays", []): + f = parse_field(entry) + member = f.get("member", f["name"]) + eid = _field_result_id_name(f, prefix=ws_rid_prefix) + lines.append( + f"{ind} if (!ws.{member}.empty()) {{ arrays[cuopt::remote::{eid}] = doubles_to_bytes(ws.{member}); }}" + ) + lines.append(f"{ind}}}") + + lines.append(f"{ind}return arrays;") + return "\n".join(lines) + + +def _gen_chunked_to_solution(registry, obj_name, obj, indent=" "): + """Generate body of chunked_result_to_{lp,mip}_solution().""" + ind = indent + lines = [] + constructor_args = obj.get("constructor_args", {}) + ws = obj.get("warm_start") + + for entry in obj.get("arrays", []): + f = parse_field(entry) + eid = _field_result_id_name(f) + lines.append( + f"{ind}auto {f['name']} = bytes_to_typed(arrays, cuopt::remote::{eid});" + ) + + lines.append("") + + scalar_vars = {} + for entry in obj.get("scalars", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + if f.get("proto_only"): + continue + ftype = f.get("type", "double") + from_cast = f.get("from_proto_cast") + edef = _lookup_enum(registry, ftype) + if edef: + from_fn = _enum_from_proto_fn(ftype, edef) + lines.append(f"{ind}auto _{name} = {from_fn}(h.{pname}());") + elif from_cast: + lines.append( + f"{ind}auto _{name} = static_cast<{from_cast}>(h.{pname}());" + ) + else: + cast = _from_proto_cast(ftype) + if cast: + lines.append( + f"{ind}auto _{name} = static_cast<{cast}>(h.{pname}());" + ) + else: + lines.append(f"{ind}auto _{name} = h.{pname}();") + scalar_vars[name] = f"_{name}" + + array_names = [parse_field(e)["name"] for e in obj.get("arrays", [])] + arg_scalars = constructor_args.get("scalars", []) + args = [f"std::move({n})" for n in array_names] + args += [scalar_vars.get(s, f"_{s}") for s in arg_scalars] + + # Warm start + if ws: + ws_arrays = ws.get("arrays", []) + ws_rid_prefix = ws.get("result_id_prefix", "") + ws_ch_prefix = ws.get("chunked_header_prefix", "") + first_array = parse_field(ws_arrays[0]) if ws_arrays else None + detect_eid = ( + _field_result_id_name(first_array, prefix=ws_rid_prefix) + if first_array + else None + ) + lines.append("") + lines.append( + f"{ind}auto _ws_detect = bytes_to_typed(arrays, cuopt::remote::{detect_eid});" + ) + lines.append(f"{ind}if (!_ws_detect.empty()) {{") + lines.append(f"{ind} cpu_pdlp_warm_start_data_t ws;") + + first = True + for entry in ws_arrays: + f = parse_field(entry) + member = f.get("member", f["name"]) + eid = _field_result_id_name(f, prefix=ws_rid_prefix) + if first: + lines.append(f"{ind} ws.{member} = std::move(_ws_detect);") + first = False + else: + lines.append( + f"{ind} ws.{member} = bytes_to_typed(arrays, cuopt::remote::{eid});" + ) + + for entry in ws.get("scalars", []): + f = parse_field(entry) + ch_name = f"{ws_ch_prefix}{f['name']}" + pname = _proto_cpp_name(ch_name) + member = f.get("member", f["name"]) + ftype = f.get("type", "double") + cast = _from_proto_cast(ftype) + if cast: + lines.append( + f"{ind} ws.{member} = static_cast<{cast}>(h.{pname}());" + ) + else: + lines.append(f"{ind} ws.{member} = h.{pname}();") + + cpp_type = obj["cpp_type"] + ws_args = args + ["std::move(ws)"] + lines.append(f"{ind} return {cpp_type}({', '.join(ws_args)});") + lines.append(f"{ind}}}") + lines.append("") + + cpp_type = obj["cpp_type"] + lines.append(f"{ind}return {cpp_type}({', '.join(args)});") + return "\n".join(lines) + + +def _gen_estimate_size(registry, obj_name, obj, indent=" "): + """Generate body of estimate_{lp,mip}_solution_proto_size().""" + ind = indent + lines = [f"{ind}size_t est = 0;"] + + for entry in obj.get("arrays", []): + f = parse_field(entry) + getter = f.get("getter", f"get_{f['name']}_host()") + size_getter = getter.replace("_host()", "_size()") + lines.append( + f"{ind}est += static_cast(solution.{size_getter}) * sizeof(double);" + ) + + ws = obj.get("warm_start") + if ws: + check = ws.get("presence_check", "has_warm_start_data()") + ws_getter = ws.get("getter", "get_cpu_pdlp_warm_start_data()") + lines.append(f"{ind}if (solution.{check}) {{") + lines.append(f"{ind} const auto& ws = solution.{ws_getter};") + for entry in ws.get("arrays", []): + f = parse_field(entry) + member = f.get("member", f["name"]) + lines.append(f"{ind} est += ws.{member}.size() * sizeof(double);") + lines.append(f"{ind}}}") + + overhead = 512 if ws else 256 + lines.append(f"{ind}est += {overhead};") + lines.append(f"{ind}return est;") + return "\n".join(lines) + + +# ============================================================================ +# Problem conversion code generation +# ============================================================================ + + +def _gen_problem_to_proto(registry, indent=" "): + """Generate body of map_problem_to_proto().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [] + setter_groups = obj.get("setter_groups", {}) + grouped_fields = set() + for gdef in setter_groups.values(): + for fname in gdef.get("fields", []): + grouped_fields.add(fname) + + # Scalars + for entry in obj.get("scalars", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + getter = _default_problem_getter(f, is_scalar=True) + ftype = f.get("type", "double") + edef = _lookup_enum(registry, ftype) + if edef: + to_fn = _enum_to_proto_fn(ftype, edef) + lines.append( + f"{ind}pb_problem->set_{pname}({to_fn}(cpu_problem.{getter}));" + ) + else: + lines.append( + f"{ind}pb_problem->set_{pname}(cpu_problem.{getter});" + ) + + lines.append("") + + # Non-grouped arrays + for entry in obj.get("arrays", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + if name in grouped_fields or f.get("skip_conversion"): + continue + ftype = f.get("type", "repeated double") + getter = _default_problem_getter(f, is_scalar=False) + + enum_key = _is_repeated_enum(registry, ftype) + if enum_key: + edef = _lookup_enum(registry, enum_key) + to_fn = _enum_to_proto_fn(enum_key, edef) + lines.append(f"{ind}{{") + lines.append(f"{ind} auto _{name} = cpu_problem.{getter};") + lines.append( + f"{ind} for (const auto& v : _{name}) pb_problem->add_{pname}({to_fn}(v));" + ) + lines.append(f"{ind}}}") + elif ftype == "repeated string": + lines.append( + f"{ind}for (const auto& s : cpu_problem.{getter}) pb_problem->add_{pname}(s);" + ) + elif ftype == "bytes": + lines.append(f"{ind}{{") + lines.append(f"{ind} auto _{name} = cpu_problem.{getter};") + lines.append(f"{ind} if (!_{name}.empty()) {{") + lines.append( + f"{ind} pb_problem->set_{pname}(std::string(_{name}.begin(), _{name}.end()));" + ) + lines.append(f"{ind} }}") + lines.append(f"{ind}}}") + elif ftype.startswith("repeated"): + cast = _array_element_cast(ftype) + var = f"_{name}" + lines.append(f"{ind}{{") + lines.append(f"{ind} auto {var} = cpu_problem.{getter};") + lines.append( + f"{ind} for (const auto& v : {var}) pb_problem->add_{pname}(static_cast<{cast}>(v));" + ) + lines.append(f"{ind}}}") + + # Setter groups + for gname, gdef in setter_groups.items(): + fields = [ + _find_problem_field(obj, fn) for fn in gdef.get("fields", []) + ] + for f in fields: + if f is None: + continue + pname = _proto_cpp_name(f["name"]) + getter = _default_problem_getter(f, is_scalar=False) + ftype = f.get("type", "repeated double") + cast = _array_element_cast(ftype) + lines.append(f"{ind}{{") + lines.append(f"{ind} auto _{f['name']} = cpu_problem.{getter};") + lines.append( + f"{ind} for (const auto& v : _{f['name']}) pb_problem->add_{pname}(static_cast<{cast}>(v));" + ) + lines.append(f"{ind}}}") + + return "\n".join(lines) + + +def _gen_proto_to_problem(registry, indent=" "): + """Generate body of map_proto_to_problem().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [] + setter_groups = obj.get("setter_groups", {}) + grouped_fields = set() + for gdef in setter_groups.values(): + for fname in gdef.get("fields", []): + grouped_fields.add(fname) + # Scalars + for entry in obj.get("scalars", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + setter = _default_problem_setter(f) + ftype = f.get("type", "double") + edef = _lookup_enum(registry, ftype) + if edef: + from_fn = _enum_from_proto_fn(ftype, edef) + lines.append( + f"{ind}cpu_problem.{setter}({from_fn}(pb_problem.{pname}()));" + ) + else: + lines.append(f"{ind}cpu_problem.{setter}(pb_problem.{pname}());") + + lines.append("") + + # Setter groups — guard on first field having data + for gname, gdef in setter_groups.items(): + setter_name = gdef["setter"] + fields = [ + _find_problem_field(obj, fn) for fn in gdef.get("fields", []) + ] + first = next((f for f in fields if f is not None), None) + if first is None: + continue + first_pname = _proto_cpp_name(first["name"]) + lines.append(f"{ind}if (pb_problem.{first_pname}_size() > 0) {{") + ii = ind + " " + + for f in fields: + if f is None: + continue + pname = _proto_cpp_name(f["name"]) + ftype = f.get("type", "repeated double") + cpp_t = _array_cpp_type(ftype) + lines.append( + f"{ii}std::vector<{cpp_t}> {f['name']}(pb_problem.{pname}().begin(), pb_problem.{pname}().end());" + ) + + args = [] + for f in fields: + if f is None: + continue + args.append(f"{f['name']}.data()") + args.append(f"static_cast({f['name']}.size())") + lines.append(f"{ii}cpu_problem.{setter_name}({', '.join(args)});") + lines.append(f"{ind}}}") + lines.append("") + + # Non-grouped arrays + for entry in obj.get("arrays", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + if name in grouped_fields or f.get("skip_conversion"): + continue + ftype = f.get("type", "repeated double") + setter = _default_problem_setter(f) + + enum_key = _is_repeated_enum(registry, ftype) + if enum_key: + edef = _lookup_enum(registry, enum_key) + from_fn = _enum_from_proto_fn(enum_key, edef) + cpp_type = _enum_cpp_type(enum_key, edef) + lines.append(f"{ind}if (pb_problem.{pname}_size() > 0) {{") + lines.append(f"{ind} std::vector<{cpp_type}> {name};") + lines.append(f"{ind} {name}.reserve(pb_problem.{pname}_size());") + proto_type = _enum_proto_type(enum_key, edef) + lines.append( + f"{ind} for (const auto& v : pb_problem.{pname}()) {{" + ) + lines.append( + f"{ind} {name}.push_back({from_fn}(static_cast(v)));" + ) + lines.append(f"{ind} }}") + lines.append( + f"{ind} cpu_problem.{setter}({name}.data(), static_cast({name}.size()));" + ) + lines.append(f"{ind}}}") + elif ftype == "repeated string": + lines.append(f"{ind}if (pb_problem.{pname}_size() > 0) {{") + lines.append( + f"{ind} std::vector {name}(pb_problem.{pname}().begin(), pb_problem.{pname}().end());" + ) + lines.append(f"{ind} cpu_problem.{setter}({name});") + lines.append(f"{ind}}}") + elif ftype == "bytes": + lines.append(f"{ind}if (!pb_problem.{pname}().empty()) {{") + lines.append( + f"{ind} const std::string& {name}_str = pb_problem.{pname}();" + ) + lines.append( + f"{ind} cpu_problem.{setter}({name}_str.data(), static_cast({name}_str.size()));" + ) + lines.append(f"{ind}}}") + elif ftype.startswith("repeated"): + cpp_t = _array_cpp_type(ftype) + guard = f.get("conditional") + open_brace = ( + f"if (pb_problem.{pname}_size() > 0) {{" if guard else "{" + ) + lines.append(f"{ind}{open_brace}") + lines.append( + f"{ind} std::vector<{cpp_t}> {name}(pb_problem.{pname}().begin(), pb_problem.{pname}().end());" + ) + lines.append( + f"{ind} cpu_problem.{setter}({name}.data(), static_cast({name}.size()));" + ) + lines.append(f"{ind}}}") + + return "\n".join(lines) + + +def _gen_estimate_problem_size(registry, indent=" "): + """Generate body of estimate_problem_proto_size().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [f"{ind}size_t est = 0;"] + + for entry in obj.get("arrays", []): + f = parse_field(entry) + if f.get("skip_conversion"): + continue + ftype = f.get("type", "repeated double") + getter = _default_problem_getter(f, is_scalar=False) + if ftype == "repeated string": + lines.append( + f"{ind}for (const auto& s : cpu_problem.{getter}) est += s.size() + 2;" + ) + elif ftype == "bytes": + lines.append(f"{ind}est += cpu_problem.{getter}.size();") + elif "int32" in ftype: + lines.append(f"{ind}est += cpu_problem.{getter}.size() * 5;") + elif ftype.startswith("repeated ") and _is_repeated_enum( + registry, ftype + ): + lines.append(f"{ind}est += cpu_problem.{getter}.size() * 4;") + else: + lines.append( + f"{ind}est += cpu_problem.{getter}.size() * sizeof(double);" + ) + + lines.append(f"{ind}est += 512;") + lines.append(f"{ind}return est;") + return "\n".join(lines) + + +def _gen_populate_chunked_header(registry, solver_type, indent=" "): + """Generate body of populate_chunked_header_{lp,mip}().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [] + + lines.append(f"{ind}auto* rh = header->mutable_header();") + lines.append(f"{ind}rh->set_version(1);") + if solver_type == "lp": + lines.append(f"{ind}rh->set_problem_category(cuopt::remote::LP);") + else: + lines.append(f"{ind}rh->set_problem_category(cuopt::remote::MIP);") + + lines.append("") + + for entry in obj.get("scalars", []): + f = parse_field(entry) + pname = _proto_cpp_name(f["name"]) + getter = _default_problem_getter(f, is_scalar=True) + ftype = f.get("type", "double") + edef = _lookup_enum(registry, ftype) + if edef: + to_fn = _enum_to_proto_fn(ftype, edef) + lines.append( + f"{ind}header->set_{pname}({to_fn}(cpu_problem.{getter}));" + ) + else: + lines.append(f"{ind}header->set_{pname}(cpu_problem.{getter});") + + lines.append("") + + if solver_type == "lp": + lines.append( + f"{ind}map_pdlp_settings_to_proto(settings, header->mutable_lp_settings());" + ) + else: + lines.append( + f"{ind}map_mip_settings_to_proto(settings, header->mutable_mip_settings());" + ) + lines.append(f"{ind}header->set_enable_incumbents(enable_incumbents);") + + return "\n".join(lines) + + +def _gen_chunked_header_to_problem(registry, indent=" "): + """Generate body of map_chunked_header_to_problem().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [] + + for entry in obj.get("scalars", []): + f = parse_field(entry) + name = f["name"] + pname = _proto_cpp_name(name) + setter = _default_problem_setter(f) + ftype = f.get("type", "double") + edef = _lookup_enum(registry, ftype) + if edef: + from_fn = _enum_from_proto_fn(ftype, edef) + lines.append( + f"{ind}cpu_problem.{setter}({from_fn}(header.{pname}()));" + ) + else: + lines.append(f"{ind}cpu_problem.{setter}(header.{pname}());") + + lines.append("") + + for entry in obj.get("arrays", []): + f = parse_field(entry) + if f.get("type") != "repeated string": + continue + name = f["name"] + pname = _proto_cpp_name(name) + setter = _default_problem_setter(f) + lines.append(f"{ind}if (header.{pname}_size() > 0) {{") + lines.append( + f"{ind} std::vector {name}(header.{pname}().begin(), header.{pname}().end());" + ) + lines.append(f"{ind} cpu_problem.{setter}({name});") + lines.append(f"{ind}}}") + + return "\n".join(lines) + + +def _gen_chunked_arrays_to_problem(registry, indent=" "): + """Generate body of map_chunked_arrays_to_problem().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [] + setter_groups = obj.get("setter_groups", {}) + grouped_fields = set() + for gdef in setter_groups.values(): + for fname in gdef.get("fields", []): + grouped_fields.add(fname) + lines.append(f"{ind}map_chunked_header_to_problem(header, cpu_problem);") + lines.append("") + + # Lambda helpers + lines.append( + f"{ind}auto get_doubles = [&](int32_t field_id) -> std::vector {{" + ) + lines.append(f"{ind} auto it = arrays.find(field_id);") + lines.append( + f"{ind} if (it == arrays.end() || it->second.empty()) return {{}};" + ) + lines.append( + f"{ind} if (it->second.size() % sizeof(double) != 0) return {{}};" + ) + lines.append(f"{ind} size_t n = it->second.size() / sizeof(double);") + lines.append(f"{ind} if constexpr (std::is_same_v) {{") + lines.append(f"{ind} std::vector v(n);") + lines.append( + f"{ind} std::memcpy(v.data(), it->second.data(), n * sizeof(double));" + ) + lines.append(f"{ind} return v;") + lines.append(f"{ind} }} else {{") + lines.append(f"{ind} std::vector tmp(n);") + lines.append( + f"{ind} std::memcpy(tmp.data(), it->second.data(), n * sizeof(double));" + ) + lines.append(f"{ind} return std::vector(tmp.begin(), tmp.end());") + lines.append(f"{ind} }}") + lines.append(f"{ind}}};") + lines.append("") + + lines.append( + f"{ind}auto get_ints = [&](int32_t field_id) -> std::vector {{" + ) + lines.append(f"{ind} auto it = arrays.find(field_id);") + lines.append( + f"{ind} if (it == arrays.end() || it->second.empty()) return {{}};" + ) + lines.append( + f"{ind} if (it->second.size() % sizeof(int32_t) != 0) return {{}};" + ) + lines.append(f"{ind} size_t n = it->second.size() / sizeof(int32_t);") + lines.append(f"{ind} if constexpr (std::is_same_v) {{") + lines.append(f"{ind} std::vector v(n);") + lines.append( + f"{ind} std::memcpy(v.data(), it->second.data(), n * sizeof(int32_t));" + ) + lines.append(f"{ind} return v;") + lines.append(f"{ind} }} else {{") + lines.append(f"{ind} std::vector tmp(n);") + lines.append( + f"{ind} std::memcpy(tmp.data(), it->second.data(), n * sizeof(int32_t));" + ) + lines.append(f"{ind} return std::vector(tmp.begin(), tmp.end());") + lines.append(f"{ind} }}") + lines.append(f"{ind}}};") + lines.append("") + + lines.append( + f"{ind}auto get_bytes = [&](int32_t field_id) -> std::string {{" + ) + lines.append(f"{ind} auto it = arrays.find(field_id);") + lines.append( + f"{ind} if (it == arrays.end() || it->second.empty()) return {{}};" + ) + lines.append( + f"{ind} return std::string(reinterpret_cast(it->second.data()), it->second.size());" + ) + lines.append(f"{ind}}};") + lines.append("") + + lines.append( + f"{ind}auto get_string_list = [&](int32_t field_id) -> std::vector {{" + ) + lines.append(f"{ind} auto it = arrays.find(field_id);") + lines.append( + f"{ind} if (it == arrays.end() || it->second.empty()) return {{}};" + ) + lines.append(f"{ind} std::vector names;") + lines.append( + f"{ind} const char* s = reinterpret_cast(it->second.data());" + ) + lines.append(f"{ind} const char* s_end = s + it->second.size();") + lines.append(f"{ind} while (s < s_end) {{") + lines.append( + f"{ind} const char* nul = static_cast(std::memchr(s, '\\0', s_end - s));" + ) + lines.append(f"{ind} if (!nul) nul = s_end;") + lines.append(f"{ind} names.emplace_back(s, nul);") + lines.append(f"{ind} if (nul == s_end) break;") + lines.append(f"{ind} s = nul + 1;") + lines.append(f"{ind} }}") + lines.append(f"{ind} return names;") + lines.append(f"{ind}}};") + lines.append("") + + # Setter groups + for gname, gdef in setter_groups.items(): + setter_name = gdef["setter"] + fields = [ + _find_problem_field(obj, fn) for fn in gdef.get("fields", []) + ] + + for f in fields: + if f is None: + continue + ftype = f.get("type", "repeated double") + afid = _field_array_id_name(f) + extract_fn = "get_doubles" if "double" in ftype else "get_ints" + lines.append( + f"{ind}auto {f['name']} = {extract_fn}(cuopt::remote::{afid});" + ) + + guard_parts = [f"!{f['name']}.empty()" for f in fields if f] + lines.append(f"{ind}if ({' && '.join(guard_parts)}) {{") + + args = [] + for f in fields: + if f is None: + continue + args.append(f"{f['name']}.data()") + args.append(f"static_cast({f['name']}.size())") + lines.append(f"{ind} cpu_problem.{setter_name}({', '.join(args)});") + lines.append(f"{ind}}}") + lines.append("") + + # Non-grouped arrays + for entry in obj.get("arrays", []): + f = parse_field(entry) + name = f["name"] + if name in grouped_fields or f.get("skip_conversion"): + continue + ftype = f.get("type", "repeated double") + afid = _field_array_id_name(f) + setter = _default_problem_setter(f) + + enum_key = _is_repeated_enum(registry, ftype) + if enum_key: + edef = _lookup_enum(registry, enum_key) + from_fn = _enum_from_proto_fn(enum_key, edef) + cpp_type = _enum_cpp_type(enum_key, edef) + lines.append( + f"{ind}auto {name}_ints = get_ints(cuopt::remote::{afid});" + ) + lines.append(f"{ind}if (!{name}_ints.empty()) {{") + lines.append(f"{ind} std::vector<{cpp_type}> {name};") + lines.append(f"{ind} {name}.reserve({name}_ints.size());") + lines.append(f"{ind} for (auto v : {name}_ints) {{") + lines.append( + f"{ind} {name}.push_back({from_fn}(static_cast(v)));" + ) + lines.append(f"{ind} }}") + lines.append( + f"{ind} cpu_problem.{setter}({name}.data(), static_cast({name}.size()));" + ) + lines.append(f"{ind}}}") + elif ftype == "repeated string": + lines.append( + f"{ind}auto {name} = get_string_list(cuopt::remote::{afid});" + ) + lines.append( + f"{ind}if (!{name}.empty()) {{ cpu_problem.{setter}({name}); }}" + ) + elif ftype == "bytes": + lines.append( + f"{ind}auto {name}_str = get_bytes(cuopt::remote::{afid});" + ) + lines.append(f"{ind}if (!{name}_str.empty()) {{") + lines.append( + f"{ind} cpu_problem.set_{name}({name}_str.data(), static_cast({name}_str.size()));" + ) + lines.append(f"{ind}}}") + elif ftype.startswith("repeated"): + extract_fn = "get_doubles" if "double" in ftype else "get_ints" + lines.append( + f"{ind}auto {name} = {extract_fn}(cuopt::remote::{afid});" + ) + lines.append(f"{ind}if (!{name}.empty()) {{") + lines.append( + f"{ind} cpu_problem.{setter}({name}.data(), static_cast({name}.size()));" + ) + lines.append(f"{ind}}}") + lines.append("") + + return "\n".join(lines).rstrip() + + +def _gen_build_array_chunk_requests(registry, indent=" "): + """Generate body of build_array_chunk_requests().""" + obj = registry.get("optimization_problem", {}) + ind = indent + lines = [ + f"{ind}std::vector requests;" + ] + lines.append("") + setter_groups = obj.get("setter_groups", {}) + grouped_fields = set() + for gdef in setter_groups.values(): + for fname in gdef.get("fields", []): + grouped_fields.add(fname) + + def _emit_chunk_call(f, getter_expr, var_name, ii): + ftype = f.get("type", "repeated double") + afid = _field_array_id_name(f) + + if ftype == "repeated string": + lines.append(f"{ii}{{") + lines.append(f"{ii} auto _blob = names_to_blob({getter_expr});") + lines.append( + f"{ii} chunk_byte_blob(requests, cuopt::remote::{afid}, _blob, upload_id, chunk_size_bytes);" + ) + lines.append(f"{ii}}}") + elif ftype == "bytes": + lines.append(f"{ii}if (!{var_name}.empty()) {{") + lines.append( + f"{ii} std::vector _bytes({var_name}.begin(), {var_name}.end());" + ) + lines.append( + f"{ii} chunk_byte_blob(requests, cuopt::remote::{afid}, _bytes, upload_id, chunk_size_bytes);" + ) + lines.append(f"{ii}}}") + elif "int32" in ftype: + lines.append( + f"{ii}chunk_typed_array(requests, cuopt::remote::{afid}, {var_name}, upload_id, chunk_size_bytes);" + ) + elif _is_repeated_enum(registry, ftype): + lines.append( + f"{ii}chunk_typed_array(requests, cuopt::remote::{afid}, {var_name}, upload_id, chunk_size_bytes);" + ) + else: + lines.append( + f"{ii}chunk_typed_array(requests, cuopt::remote::{afid}, {var_name}, upload_id, chunk_size_bytes);" + ) + + # Non-grouped arrays + for entry in obj.get("arrays", []): + f = parse_field(entry) + name = f["name"] + if name in grouped_fields or f.get("skip_conversion"): + continue + ftype = f.get("type", "repeated double") + getter = _default_problem_getter(f, is_scalar=False) + + enum_key = _is_repeated_enum(registry, ftype) + if enum_key: + edef = _lookup_enum(registry, enum_key) + to_fn = _enum_to_proto_fn(enum_key, edef) + afid = _field_array_id_name(f) + lines.append(f"{ind}{{") + lines.append(f"{ind} auto _{name} = problem.{getter};") + lines.append(f"{ind} if (!_{name}.empty()) {{") + lines.append(f"{ind} std::vector _{name}_ints;") + lines.append(f"{ind} _{name}_ints.reserve(_{name}.size());") + lines.append( + f"{ind} for (const auto& v : _{name}) _{name}_ints.push_back(static_cast({to_fn}(v)));" + ) + lines.append( + f"{ind} chunk_typed_array(requests, cuopt::remote::{afid}, _{name}_ints, upload_id, chunk_size_bytes);" + ) + lines.append(f"{ind} }}") + lines.append(f"{ind}}}") + elif ftype == "repeated string": + _emit_chunk_call(f, f"problem.{getter}", None, ind) + elif ftype == "bytes": + lines.append(f"{ind}{{") + lines.append(f"{ind} auto _{name} = problem.{getter};") + _emit_chunk_call(f, None, f"_{name}", ind + " ") + lines.append(f"{ind}}}") + else: + lines.append(f"{ind}{{") + lines.append(f"{ind} auto _{name} = problem.{getter};") + _emit_chunk_call(f, None, f"_{name}", ind + " ") + lines.append(f"{ind}}}") + + # Setter groups + for gname, gdef in setter_groups.items(): + fields = [ + _find_problem_field(obj, fn) for fn in gdef.get("fields", []) + ] + for f in fields: + if f is None: + continue + getter = _default_problem_getter(f, is_scalar=False) + afid = _field_array_id_name(f) + lines.append(f"{ind}{{") + lines.append( + f"{ind} const auto& _{f['name']} = problem.{getter};" + ) + lines.append( + f"{ind} chunk_typed_array(requests, cuopt::remote::{afid}, _{f['name']}, upload_id, chunk_size_bytes);" + ) + lines.append(f"{ind}}}") + + lines.append("") + lines.append(f"{ind}return requests;") + return "\n".join(lines) + + +# ============================================================================ +# Auto-numbering (ruamel.yaml round-trip) +# ============================================================================ + +# Ranges for auto-numbering. hi=None means no upper bound (own message). +# Only solution scalars need hard caps because they share ChunkedResultHeader. +# Solution array_ids share a global ResultFieldId pool (handled separately). +FIELD_NUM_RANGES = { + "optimization_problem.field_num": (1, None), + "optimization_problem.array_id": (0, None), + "lp_solution.scalars": (1000, 1999), + "lp_solution.arrays.field_num": (1, None), + "mip_solution.scalars": (2000, 2999), + "mip_solution.arrays.field_num": (1, None), + "lp_solution.warm_start.scalars": (3000, 3999), + "lp_solution.warm_start.arrays.field_num": (1, None), + "pdlp_settings": (1, None), + "mip_settings": (1, None), +} + + +def _collect_field_nums(entries, key_name="field_num"): + """Collect all existing field_num or array_id values from a list of field entries.""" + nums = set() + for entry in entries: + if isinstance(entry, dict) and len(entry) == 1: + name = list(entry.keys())[0] + val = entry[name] + if isinstance(val, dict) and key_name in val: + nums.add(val[key_name]) + return nums + + +def _collect_settings_field_nums(entries): + """Collect field_num values from settings fields (handles nesting).""" + nums = set() + for entry in entries: + if isinstance(entry, dict) and len(entry) == 1: + name = list(entry.keys())[0] + val = entry[name] + if isinstance(val, list): + nums |= _collect_settings_field_nums(val) + elif isinstance(val, dict) and "field_num" in val: + nums.add(val["field_num"]) + return nums + + +def _ruamel_insert(mapping, key, value): + """Add a key to a CommentedMap at its conventional position. + + field_num goes first (position 0), array_id goes right after field_num, + everything else appends.""" + if not hasattr(mapping, "insert"): + mapping[key] = value + return + if key == "field_num": + mapping.insert(0, key, value) + elif key == "array_id": + pos = 1 if "field_num" in mapping else 0 + mapping.insert(pos, key, value) + else: + mapping[key] = value + + +def _assign_to_field_list(entries, key_name, lo, hi, existing, label): + """Assign missing key_name values to fields in a list. Mutates entries in-place. + hi=None means no upper bound.""" + next_num = max(existing) + 1 if existing else lo + assigned = 0 + for idx, entry in enumerate(entries): + if isinstance(entry, str): + continue + if not isinstance(entry, dict) or len(entry) != 1: + continue + name = list(entry.keys())[0] + val = entry[name] + if val is None: + try: + from ruamel.yaml.comments import CommentedMap + + entry[name] = CommentedMap([(key_name, next_num)]) + except ImportError: + entry[name] = {key_name: next_num} + existing.add(next_num) + next_num += 1 + assigned += 1 + elif isinstance(val, dict) and key_name not in val: + _ruamel_insert(val, key_name, next_num) + existing.add(next_num) + next_num += 1 + assigned += 1 + if hi is not None and next_num - 1 > hi and assigned > 0: + raise ValueError( + f"Range overflow in {label}: assigned {next_num - 1} exceeds max {hi}" + ) + return assigned + + +def _assign_to_settings_fields(entries, lo, hi, existing, label): + """Assign missing field_num to settings fields (handles nesting). Mutates in-place. + hi=None means no upper bound.""" + try: + from ruamel.yaml.comments import CommentedMap as _CMap + except ImportError: + _CMap = None + next_num = max(existing) + 1 if existing else lo + assigned = 0 + for entry in entries: + if not isinstance(entry, dict) or len(entry) != 1: + continue + name = list(entry.keys())[0] + val = entry[name] + if isinstance(val, list): + sub_assigned = _assign_to_settings_fields( + val, lo, hi, existing, f"{label}.{name}" + ) + assigned += sub_assigned + next_num = max(existing) + 1 if existing else next_num + elif val is None: + if _CMap is not None: + entry[name] = _CMap([("field_num", next_num)]) + else: + entry[name] = {"field_num": next_num} + existing.add(next_num) + next_num += 1 + assigned += 1 + elif isinstance(val, dict) and "field_num" not in val: + _ruamel_insert(val, "field_num", next_num) + existing.add(next_num) + next_num += 1 + assigned += 1 + if hi is not None and next_num - 1 > hi and assigned > 0: + raise ValueError( + f"Range overflow in {label}: assigned {next_num - 1} exceeds max {hi}" + ) + return assigned + + +def _normalize_bare_strings(entries): + """Convert bare string entries in a field list to single-key dicts. + This lets the auto-numbering logic treat them uniformly. + + Avoids modifying CommentedSeq comment attributes (ca.items) to prevent + corrupting ruamel.yaml's internal comment tracking on dump.""" + try: + from ruamel.yaml.comments import CommentedMap + + mk_inner = CommentedMap + + def mk_outer(name, inner): + return CommentedMap([(name, inner)]) + + except ImportError: + mk_inner = dict + + def mk_outer(name, inner): + return {name: inner} + + for idx, entry in enumerate(entries): + if isinstance(entry, str): + entries[idx] = mk_outer(entry, mk_inner()) + + +_STRIP_RE = re.compile(r"^ +(?:field_num|array_id): +\d+ *\n", re.MULTILINE) + + +def strip_field_numbers_text(path): + """Remove field_num/array_id lines from the registry via text substitution. + + Pure text operation — guaranteed to preserve all comments and formatting. + Returns the number of lines removed.""" + with open(path) as f: + original = f.read() + stripped, count = _STRIP_RE.subn("", original) + if count > 0: + with open(path, "w") as f: + f.write(stripped) + return count + + +def _dump_and_verify(ryaml, data, path): + """Dump YAML data to a string buffer, verify comments survived, then write. + + Never truncates the original file until the output is validated.""" + from io import StringIO + + with open(path) as f: + original = f.read() + original_comments = [ + line for line in original.splitlines() if line.lstrip().startswith("#") + ] + + buf = StringIO() + ryaml.dump(data, buf) + written = buf.getvalue() + written_comments = [ + line for line in written.splitlines() if line.lstrip().startswith("#") + ] + + if len(written_comments) < len(original_comments): + print( + f"ERROR: comment loss detected in {path} " + f"({len(original_comments)} → {len(written_comments)} comment lines). " + f"File not modified.", + file=sys.stderr, + ) + sys.exit(1) + + with open(path, "w") as f: + f.write(written) + + +def _registry_has_field_numbers(registry): + """Check if the registry has at least one field_num or array_id assigned.""" + for section_key in list(registry.keys()): + section = registry.get(section_key) + if not isinstance(section, dict): + continue + for list_key in ["scalars", "arrays"]: + for nums in [ + _collect_field_nums(section.get(list_key, []), "field_num"), + _collect_field_nums(section.get(list_key, []), "array_id"), + ]: + if nums: + return True + if "fields" in section: + if _collect_settings_field_nums(section["fields"]): + return True + sub = section.get("warm_start") + if isinstance(sub, dict): + for list_key in ["scalars", "arrays"]: + for key in ["field_num", "array_id"]: + if _collect_field_nums(sub.get(list_key, []), key): + return True + return False + + +def auto_assign_field_numbers(data): + """Walk the registry and assign missing field_num / array_id values. + + Operates on ruamel.yaml CommentedMap data in-place. + Returns the total number of assignments made. + """ + # Normalize bare string entries to dicts before processing + for section_key in list(data.keys()): + section = data.get(section_key) + if not isinstance(section, dict): + continue + for list_key in ["scalars", "arrays", "fields"]: + entries = section.get(list_key) + if entries: + _normalize_bare_strings(entries) + for sub_key in ["warm_start"]: + sub = section.get(sub_key) + if isinstance(sub, dict): + for list_key in ["scalars", "arrays"]: + entries = sub.get(list_key) + if entries: + _normalize_bare_strings(entries) + + total = 0 + + for section_key in ["optimization_problem"]: + section = data.get(section_key) + if not section: + continue + + # field_num — single contiguous pool across scalars and arrays + scalars = section.get("scalars", []) + arrays = section.get("arrays", []) + lo, hi = FIELD_NUM_RANGES[f"{section_key}.field_num"] + existing_fn = _collect_field_nums( + scalars, "field_num" + ) | _collect_field_nums(arrays, "field_num") + total += _assign_to_field_list( + scalars, + "field_num", + lo, + hi, + existing_fn, + f"{section_key}.scalars.field_num", + ) + total += _assign_to_field_list( + arrays, + "field_num", + lo, + hi, + existing_fn, + f"{section_key}.arrays.field_num", + ) + + # array_id — separate namespace + lo, hi = FIELD_NUM_RANGES[f"{section_key}.array_id"] + existing_aid = _collect_field_nums(arrays, "array_id") + total += _assign_to_field_list( + arrays, + "array_id", + lo, + hi, + existing_aid, + f"{section_key}.arrays.array_id", + ) + + # Collect all solution array_ids into a shared pool (ResultFieldId is global). + shared_result_ids = set() + all_solution_array_lists = [] + for section_key in ["lp_solution", "mip_solution"]: + section = data.get(section_key) + if not section: + continue + arr = section.get("arrays", []) + shared_result_ids |= _collect_field_nums(arr, "array_id") + all_solution_array_lists.append((section_key, arr)) + ws = section.get("warm_start") + if ws: + ws_arr = ws.get("arrays", []) + shared_result_ids |= _collect_field_nums(ws_arr, "array_id") + all_solution_array_lists.append( + (f"{section_key}.warm_start", ws_arr) + ) + + for section_key in ["lp_solution", "mip_solution"]: + section = data.get(section_key) + if not section: + continue + + # Scalars — field_num (range-bound for ChunkedResultHeader) + scalars = section.get("scalars", []) + lo, hi = FIELD_NUM_RANGES[f"{section_key}.scalars"] + existing = _collect_field_nums(scalars, "field_num") + total += _assign_to_field_list( + scalars, "field_num", lo, hi, existing, f"{section_key}.scalars" + ) + + # Arrays — field_num (per-message, no cap) + arrays = section.get("arrays", []) + arr_fn_range = FIELD_NUM_RANGES.get( + f"{section_key}.arrays.field_num", (1, None) + ) + existing_fn = _collect_field_nums(arrays, "field_num") + total += _assign_to_field_list( + arrays, + "field_num", + arr_fn_range[0], + arr_fn_range[1], + existing_fn, + f"{section_key}.arrays.field_num", + ) + + # Arrays — array_id (shared ResultFieldId pool, no cap) + total += _assign_to_field_list( + arrays, + "array_id", + 0, + None, + shared_result_ids, + f"{section_key}.arrays.array_id", + ) + + # Warm start + ws = section.get("warm_start") + if ws: + ws_scalars = ws.get("scalars", []) + lo, hi = FIELD_NUM_RANGES.get( + f"{section_key}.warm_start.scalars", (3000, 3999) + ) + existing = _collect_field_nums(ws_scalars, "field_num") + total += _assign_to_field_list( + ws_scalars, + "field_num", + lo, + hi, + existing, + f"{section_key}.warm_start.scalars", + ) + + ws_arrays = ws.get("arrays", []) + ws_fn_range = FIELD_NUM_RANGES.get( + f"{section_key}.warm_start.arrays.field_num", (1, None) + ) + existing_fn = _collect_field_nums(ws_arrays, "field_num") + total += _assign_to_field_list( + ws_arrays, + "field_num", + ws_fn_range[0], + ws_fn_range[1], + existing_fn, + f"{section_key}.warm_start.arrays.field_num", + ) + + # WS arrays — array_id (shared ResultFieldId pool) + total += _assign_to_field_list( + ws_arrays, + "array_id", + 0, + None, + shared_result_ids, + f"{section_key}.warm_start.arrays.array_id", + ) + + for section_key in ["pdlp_settings", "mip_settings"]: + section = data.get(section_key) + if not section: + continue + fields = section.get("fields", []) + lo, hi = FIELD_NUM_RANGES[section_key] + existing = _collect_settings_field_nums(fields) + total += _assign_to_settings_fields( + fields, lo, hi, existing, section_key + ) + + return total + + +# ============================================================================ +# main() +# ============================================================================ + + +def main(): + parser = argparse.ArgumentParser( + description="Generate conversion code from field registry" + ) + parser.add_argument( + "--registry", + default=str(Path(__file__).parent / "field_registry.yaml"), + ) + parser.add_argument( + "--output-dir", default=str(Path(__file__).parent / "generated") + ) + parser.add_argument( + "--auto-number", + action="store_true", + help="Auto-assign missing field_num and array_id values, writing " + "them back to the registry YAML (preserves comments/formatting).", + ) + parser.add_argument( + "--strip", + action="store_true", + help="Remove all field_num and array_id values from the registry YAML " + "and exit. Useful before committing a minimal registry.", + ) + args = parser.parse_args() + + if args.strip: + n = strip_field_numbers_text(args.registry) + if n > 0: + print(f" stripped {n} field number(s) from {args.registry}") + else: + print(" no field numbers to strip") + sys.exit(0) + + if args.auto_number: + try: + from ruamel.yaml import YAML + except ImportError: + print( + "ERROR: --auto-number requires ruamel.yaml. " + "Install with: pip install ruamel.yaml", + file=sys.stderr, + ) + sys.exit(1) + + ryaml = YAML(typ="rt") + ryaml.preserve_quotes = True + with open(args.registry) as f: + rt_data = ryaml.load(f) + + n = auto_assign_field_numbers(rt_data) + if n > 0: + _dump_and_verify(ryaml, rt_data, args.registry) + print(f" auto-numbered {n} field(s) in {args.registry}") + else: + print(" all field numbers already assigned") + + with open(args.registry) as f: + registry = yaml.safe_load(f) + + if not _registry_has_field_numbers(registry): + print( + "ERROR: Registry has no field_num or array_id values assigned.\n" + "Run with --auto-number to assign them before generating.", + file=sys.stderr, + ) + sys.exit(1) + + outdir = args.output_dir + + # Proto reference .inc files + write_file( + os.path.join(outdir, "generated_result_enums.proto.inc"), + HEADER + + "// ResultFieldId enum entries\n" + + generate_proto_result_enums(registry) + + "\n", + ) + + # Settings conversion .inc + for key, label in [("pdlp_settings", "pdlp"), ("mip_settings", "mip")]: + if key in registry: + obj = registry[key] + write_file( + os.path.join( + outdir, f"generated_{label}_settings_to_proto.inc" + ), + HEADER + + generate_settings_to_proto_body(registry, key, obj) + + "\n", + ) + write_file( + os.path.join( + outdir, f"generated_proto_to_{label}_settings.inc" + ), + HEADER + + generate_proto_to_settings_body(registry, key, obj) + + "\n", + ) + + # Full data proto + write_file( + os.path.join(outdir, "cuopt_remote_data.proto"), + generate_data_proto(registry), + ) + + # Per-domain enum converters + for domain in ("settings", "solution", "problem"): + write_file( + os.path.join(outdir, f"generated_enum_converters_{domain}.inc"), + HEADER + generate_enum_converters_inc(registry, domain) + "\n", + ) + + # Solution conversion .inc files + for key, label in [("lp_solution", "lp"), ("mip_solution", "mip")]: + obj = registry.get(key) + if not obj: + continue + write_file( + os.path.join(outdir, f"generated_{label}_solution_to_proto.inc"), + HEADER + _gen_solution_to_proto(registry, key, obj) + "\n", + ) + write_file( + os.path.join(outdir, f"generated_proto_to_{label}_solution.inc"), + HEADER + _gen_proto_to_solution(registry, key, obj) + "\n", + ) + write_file( + os.path.join(outdir, f"generated_{label}_chunked_header.inc"), + HEADER + _gen_chunked_header(registry, key, obj) + "\n", + ) + write_file( + os.path.join(outdir, f"generated_collect_{label}_arrays.inc"), + HEADER + _gen_collect_arrays(registry, key, obj) + "\n", + ) + write_file( + os.path.join(outdir, f"generated_chunked_to_{label}_solution.inc"), + HEADER + _gen_chunked_to_solution(registry, key, obj) + "\n", + ) + write_file( + os.path.join(outdir, f"generated_estimate_{label}_size.inc"), + HEADER + _gen_estimate_size(registry, key, obj) + "\n", + ) + + # Problem conversion .inc files + if "optimization_problem" in registry: + write_file( + os.path.join(outdir, "generated_problem_to_proto.inc"), + HEADER + _gen_problem_to_proto(registry) + "\n", + ) + write_file( + os.path.join(outdir, "generated_proto_to_problem.inc"), + HEADER + _gen_proto_to_problem(registry) + "\n", + ) + write_file( + os.path.join(outdir, "generated_estimate_problem_size.inc"), + HEADER + _gen_estimate_problem_size(registry) + "\n", + ) + write_file( + os.path.join(outdir, "generated_populate_chunked_header_lp.inc"), + HEADER + _gen_populate_chunked_header(registry, "lp") + "\n", + ) + write_file( + os.path.join(outdir, "generated_populate_chunked_header_mip.inc"), + HEADER + _gen_populate_chunked_header(registry, "mip") + "\n", + ) + write_file( + os.path.join(outdir, "generated_chunked_header_to_problem.inc"), + HEADER + _gen_chunked_header_to_problem(registry) + "\n", + ) + write_file( + os.path.join(outdir, "generated_chunked_arrays_to_problem.inc"), + HEADER + _gen_chunked_arrays_to_problem(registry) + "\n", + ) + write_file( + os.path.join(outdir, "generated_build_array_chunks.inc"), + HEADER + _gen_build_array_chunk_requests(registry) + "\n", + ) + write_file( + os.path.join(outdir, "generated_array_field_element_size.inc"), + HEADER + generate_array_field_element_size_inc(registry) + "\n", + ) + + print(f"\nDone! Generated {len(os.listdir(outdir))} files in: {outdir}") + + +if __name__ == "__main__": + main() diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto new file mode 100644 index 0000000000..09249ba8f5 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -0,0 +1,287 @@ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT — regenerate with: python cpp/src/grpc/codegen/generate_conversions.py + +syntax = "proto3"; + +package cuopt.remote; + +enum PDLPTerminationStatus { + PDLP_NO_TERMINATION = 0; + PDLP_NUMERICAL_ERROR = 1; + PDLP_OPTIMAL = 2; + PDLP_PRIMAL_INFEASIBLE = 3; + PDLP_DUAL_INFEASIBLE = 4; + PDLP_ITERATION_LIMIT = 5; + PDLP_TIME_LIMIT = 6; + PDLP_CONCURRENT_LIMIT = 7; + PDLP_PRIMAL_FEASIBLE = 8; +} + +enum MIPTerminationStatus { + MIP_NO_TERMINATION = 0; + MIP_OPTIMAL = 1; + MIP_FEASIBLE_FOUND = 2; + MIP_INFEASIBLE = 3; + MIP_UNBOUNDED = 4; + MIP_TIME_LIMIT = 5; + MIP_WORK_LIMIT = 6; +} + +enum PDLPSolverMode { + Stable1 = 0; + Stable2 = 1; + Methodical1 = 2; + Fast1 = 3; + Stable3 = 4; +} + +enum LPMethod { + Concurrent = 0; + PDLP = 1; + DualSimplex = 2; + Barrier = 3; +} + +enum VariableType { + CONTINUOUS = 0; + INTEGER = 1; +} + +enum ProblemCategory { + LP = 0; + MIP = 1; +} + +enum ResultFieldId { + option allow_alias = true; + RESULT_PRIMAL_SOLUTION = 0; + RESULT_DUAL_SOLUTION = 1; + RESULT_REDUCED_COST = 2; + RESULT_WS_CURRENT_PRIMAL_SOLUTION = 3; + RESULT_WS_CURRENT_DUAL_SOLUTION = 4; + RESULT_WS_INITIAL_PRIMAL_AVERAGE = 5; + RESULT_WS_INITIAL_DUAL_AVERAGE = 6; + RESULT_WS_CURRENT_ATY = 7; + RESULT_WS_SUM_PRIMAL_SOLUTIONS = 8; + RESULT_WS_SUM_DUAL_SOLUTIONS = 9; + RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION = 10; + RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION = 11; + RESULT_MIP_SOLUTION = 12; + RESULT_WS_CURRENT_PRIMAL = 3; + RESULT_WS_CURRENT_DUAL = 4; + RESULT_WS_INITIAL_PRIMAL_AVG = 5; + RESULT_WS_INITIAL_DUAL_AVG = 6; + RESULT_WS_SUM_PRIMAL = 8; + RESULT_WS_SUM_DUAL = 9; + RESULT_WS_LAST_RESTART_GAP_PRIMAL = 10; + RESULT_WS_LAST_RESTART_GAP_DUAL = 11; +} + +enum ArrayFieldId { + FIELD_VARIABLE_NAMES = 0; + FIELD_ROW_NAMES = 1; + FIELD_A_VALUES = 2; + FIELD_A_INDICES = 3; + FIELD_A_OFFSETS = 4; + FIELD_C = 5; + FIELD_B = 6; + FIELD_VARIABLE_LOWER_BOUNDS = 7; + FIELD_VARIABLE_UPPER_BOUNDS = 8; + FIELD_CONSTRAINT_LOWER_BOUNDS = 9; + FIELD_CONSTRAINT_UPPER_BOUNDS = 10; + FIELD_ROW_TYPES = 11; + FIELD_VARIABLE_TYPES = 12; + FIELD_INITIAL_PRIMAL_SOLUTION = 13; + FIELD_INITIAL_DUAL_SOLUTION = 14; + FIELD_Q_VALUES = 15; + FIELD_Q_INDICES = 16; + FIELD_Q_OFFSETS = 17; +} + +message OptimizationProblem { + string problem_name = 1; + string objective_name = 2; + bool maximize = 3; + double objective_scaling_factor = 4; + double objective_offset = 5; + repeated string variable_names = 7; + repeated string row_names = 8; + repeated double A_values = 9; + repeated int32 A_indices = 10; + repeated int32 A_offsets = 11; + repeated double c = 12; + repeated double b = 13; + repeated double variable_lower_bounds = 14; + repeated double variable_upper_bounds = 15; + repeated double constraint_lower_bounds = 16; + repeated double constraint_upper_bounds = 17; + bytes row_types = 18; + repeated VariableType variable_types = 19; + repeated double initial_primal_solution = 20; + repeated double initial_dual_solution = 21; + repeated double Q_values = 22; + repeated int32 Q_indices = 23; + repeated int32 Q_offsets = 24; +} + +message PDLPSolverSettings { + double absolute_gap_tolerance = 1; + double relative_gap_tolerance = 2; + double primal_infeasible_tolerance = 3; + double dual_infeasible_tolerance = 4; + double absolute_dual_tolerance = 5; + double relative_dual_tolerance = 6; + double absolute_primal_tolerance = 7; + double relative_primal_tolerance = 8; + double time_limit = 9; + int64 iteration_limit = 10; + bool log_to_console = 11; + bool detect_infeasibility = 12; + bool strict_infeasibility = 13; + PDLPSolverMode pdlp_solver_mode = 14; + LPMethod method = 15; + int32 presolver = 16; + bool dual_postsolve = 17; + bool crossover = 18; + int32 num_gpus = 19; + bool per_constraint_residual = 20; + bool cudss_deterministic = 21; + int32 folding = 22; + int32 augmented = 23; + int32 dualize = 24; + int32 ordering = 25; + int32 barrier_dual_initial_point = 26; + bool eliminate_dense_columns = 27; + bool save_best_primal_so_far = 28; + bool first_primal_feasible = 29; + int32 pdlp_precision = 30; + PDLPWarmStartData warm_start_data = 50; +} + +message MIPSolverSettings { + double time_limit = 1; + double relative_mip_gap = 2; + double absolute_mip_gap = 3; + double integrality_tolerance = 4; + double absolute_tolerance = 5; + double relative_tolerance = 6; + double presolve_absolute_tolerance = 7; + bool log_to_console = 8; + bool heuristics_only = 9; + int32 num_cpu_threads = 10; + int32 num_gpus = 11; + int32 presolver = 12; + int32 mip_scaling = 13; + double work_limit = 14; + int32 node_limit = 15; + int32 reliability_branching = 16; + int32 mip_batch_pdlp_strong_branching = 17; + int32 max_cut_passes = 18; + int32 mir_cuts = 19; + int32 mixed_integer_gomory_cuts = 20; + int32 knapsack_cuts = 21; + int32 clique_cuts = 22; + int32 strong_chvatal_gomory_cuts = 23; + int32 reduced_cost_strengthening = 24; + double cut_change_threshold = 25; + double cut_min_orthogonality = 26; + int32 determinism_mode = 27; + int32 seed = 28; + int32 mip_batch_pdlp_reliability_branching = 29; + int32 strong_branching_simplex_iteration_limit = 30; + int32 implied_bound_cuts = 31; +} + +message PDLPWarmStartData { + repeated double current_primal_solution = 1; + repeated double current_dual_solution = 2; + repeated double initial_primal_average = 3; + repeated double initial_dual_average = 4; + repeated double current_ATY = 5; + repeated double sum_primal_solutions = 6; + repeated double sum_dual_solutions = 7; + repeated double last_restart_duality_gap_primal_solution = 8; + repeated double last_restart_duality_gap_dual_solution = 9; + double initial_primal_weight = 3000; + double initial_step_size = 3001; + int32 total_pdlp_iterations = 3002; + int32 total_pdhg_iterations = 3003; + double last_candidate_kkt_score = 3004; + double last_restart_kkt_score = 3005; + double sum_solution_weight = 3006; + int32 iterations_since_last_restart = 3007; +} + +message LPSolution { + repeated double primal_solution = 1; + repeated double dual_solution = 2; + repeated double reduced_cost = 3; + PDLPWarmStartData warm_start_data = 4; + PDLPTerminationStatus lp_termination_status = 1000; + string error_message = 1001; + double l2_primal_residual = 1002; + double l2_dual_residual = 1003; + double primal_objective = 1004; + double dual_objective = 1005; + double gap = 1006; + int32 nb_iterations = 1007; + double solve_time = 1008; + int32 solved_by = 1009; +} + +message MIPSolution { + repeated double mip_solution = 1; + MIPTerminationStatus mip_termination_status = 2000; + string mip_error_message = 2001; + double mip_objective = 2002; + double mip_gap = 2003; + double solution_bound = 2004; + double total_solve_time = 2005; + double presolve_time = 2006; + double max_constraint_violation = 2007; + double max_int_violation = 2008; + double max_variable_bound_violation = 2009; + int32 nodes = 2010; + int32 simplex_iterations = 2011; +} + +message ResultArrayDescriptor { + ResultFieldId field_id = 1; + int64 total_elements = 2; + int64 element_size_bytes = 3; +} + +message ChunkedResultHeader { + ProblemCategory problem_category = 1; + repeated ResultArrayDescriptor arrays = 50; + PDLPTerminationStatus lp_termination_status = 1000; + string error_message = 1001; + double l2_primal_residual = 1002; + double l2_dual_residual = 1003; + double primal_objective = 1004; + double dual_objective = 1005; + double gap = 1006; + int32 nb_iterations = 1007; + double solve_time = 1008; + int32 solved_by = 1009; + MIPTerminationStatus mip_termination_status = 2000; + string mip_error_message = 2001; + double mip_objective = 2002; + double mip_gap = 2003; + double solution_bound = 2004; + double total_solve_time = 2005; + double presolve_time = 2006; + double max_constraint_violation = 2007; + double max_int_violation = 2008; + double max_variable_bound_violation = 2009; + int32 nodes = 2010; + int32 simplex_iterations = 2011; + double ws_initial_primal_weight = 3000; + double ws_initial_step_size = 3001; + int32 ws_total_pdlp_iterations = 3002; + int32 ws_total_pdhg_iterations = 3003; + double ws_last_candidate_kkt_score = 3004; + double ws_last_restart_kkt_score = 3005; + double ws_sum_solution_weight = 3006; + int32 ws_iterations_since_last_restart = 3007; +} diff --git a/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc b/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc new file mode 100644 index 0000000000..b1eb078ccd --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc @@ -0,0 +1,17 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + switch (field_id) { + case cuopt::remote::FIELD_VARIABLE_NAMES: + case cuopt::remote::FIELD_ROW_NAMES: + case cuopt::remote::FIELD_ROW_TYPES: + return 1; + case cuopt::remote::FIELD_A_INDICES: + case cuopt::remote::FIELD_A_OFFSETS: + case cuopt::remote::FIELD_VARIABLE_TYPES: + case cuopt::remote::FIELD_Q_INDICES: + case cuopt::remote::FIELD_Q_OFFSETS: + return 4; + default: return 8; + } diff --git a/cpp/src/grpc/codegen/generated/generated_build_array_chunks.inc b/cpp/src/grpc/codegen/generated/generated_build_array_chunks.inc new file mode 100644 index 0000000000..638635e021 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_build_array_chunks.inc @@ -0,0 +1,80 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + std::vector requests; + + { + auto _blob = names_to_blob(problem.get_variable_names()); + chunk_byte_blob(requests, cuopt::remote::FIELD_VARIABLE_NAMES, _blob, upload_id, chunk_size_bytes); + } + { + auto _blob = names_to_blob(problem.get_row_names()); + chunk_byte_blob(requests, cuopt::remote::FIELD_ROW_NAMES, _blob, upload_id, chunk_size_bytes); + } + { + auto _c = problem.get_objective_coefficients_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_C, _c, upload_id, chunk_size_bytes); + } + { + auto _b = problem.get_constraint_bounds_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_B, _b, upload_id, chunk_size_bytes); + } + { + auto _variable_lower_bounds = problem.get_variable_lower_bounds_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_VARIABLE_LOWER_BOUNDS, _variable_lower_bounds, upload_id, chunk_size_bytes); + } + { + auto _variable_upper_bounds = problem.get_variable_upper_bounds_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_VARIABLE_UPPER_BOUNDS, _variable_upper_bounds, upload_id, chunk_size_bytes); + } + { + auto _constraint_lower_bounds = problem.get_constraint_lower_bounds_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_CONSTRAINT_LOWER_BOUNDS, _constraint_lower_bounds, upload_id, chunk_size_bytes); + } + { + auto _constraint_upper_bounds = problem.get_constraint_upper_bounds_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_CONSTRAINT_UPPER_BOUNDS, _constraint_upper_bounds, upload_id, chunk_size_bytes); + } + { + auto _row_types = problem.get_row_types_host(); + if (!_row_types.empty()) { + std::vector _bytes(_row_types.begin(), _row_types.end()); + chunk_byte_blob(requests, cuopt::remote::FIELD_ROW_TYPES, _bytes, upload_id, chunk_size_bytes); + } + } + { + auto _variable_types = problem.get_variable_types_host(); + if (!_variable_types.empty()) { + std::vector _variable_types_ints; + _variable_types_ints.reserve(_variable_types.size()); + for (const auto& v : _variable_types) _variable_types_ints.push_back(static_cast(to_proto_variable_type(v))); + chunk_typed_array(requests, cuopt::remote::FIELD_VARIABLE_TYPES, _variable_types_ints, upload_id, chunk_size_bytes); + } + } + { + const auto& _A_values = problem.get_constraint_matrix_values_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_A_VALUES, _A_values, upload_id, chunk_size_bytes); + } + { + const auto& _A_indices = problem.get_constraint_matrix_indices_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_A_INDICES, _A_indices, upload_id, chunk_size_bytes); + } + { + const auto& _A_offsets = problem.get_constraint_matrix_offsets_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_A_OFFSETS, _A_offsets, upload_id, chunk_size_bytes); + } + { + const auto& _Q_values = problem.get_quadratic_objective_values_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_Q_VALUES, _Q_values, upload_id, chunk_size_bytes); + } + { + const auto& _Q_indices = problem.get_quadratic_objective_indices_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_Q_INDICES, _Q_indices, upload_id, chunk_size_bytes); + } + { + const auto& _Q_offsets = problem.get_quadratic_objective_offsets_host(); + chunk_typed_array(requests, cuopt::remote::FIELD_Q_OFFSETS, _Q_offsets, upload_id, chunk_size_bytes); + } + + return requests; diff --git a/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc b/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc new file mode 100644 index 0000000000..f3ed757c8d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc @@ -0,0 +1,124 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + map_chunked_header_to_problem(header, cpu_problem); + + auto get_doubles = [&](int32_t field_id) -> std::vector { + auto it = arrays.find(field_id); + if (it == arrays.end() || it->second.empty()) return {}; + if (it->second.size() % sizeof(double) != 0) return {}; + size_t n = it->second.size() / sizeof(double); + if constexpr (std::is_same_v) { + std::vector v(n); + std::memcpy(v.data(), it->second.data(), n * sizeof(double)); + return v; + } else { + std::vector tmp(n); + std::memcpy(tmp.data(), it->second.data(), n * sizeof(double)); + return std::vector(tmp.begin(), tmp.end()); + } + }; + + auto get_ints = [&](int32_t field_id) -> std::vector { + auto it = arrays.find(field_id); + if (it == arrays.end() || it->second.empty()) return {}; + if (it->second.size() % sizeof(int32_t) != 0) return {}; + size_t n = it->second.size() / sizeof(int32_t); + if constexpr (std::is_same_v) { + std::vector v(n); + std::memcpy(v.data(), it->second.data(), n * sizeof(int32_t)); + return v; + } else { + std::vector tmp(n); + std::memcpy(tmp.data(), it->second.data(), n * sizeof(int32_t)); + return std::vector(tmp.begin(), tmp.end()); + } + }; + + auto get_bytes = [&](int32_t field_id) -> std::string { + auto it = arrays.find(field_id); + if (it == arrays.end() || it->second.empty()) return {}; + return std::string(reinterpret_cast(it->second.data()), it->second.size()); + }; + + auto get_string_list = [&](int32_t field_id) -> std::vector { + auto it = arrays.find(field_id); + if (it == arrays.end() || it->second.empty()) return {}; + std::vector names; + const char* s = reinterpret_cast(it->second.data()); + const char* s_end = s + it->second.size(); + while (s < s_end) { + const char* nul = static_cast(std::memchr(s, '\0', s_end - s)); + if (!nul) nul = s_end; + names.emplace_back(s, nul); + if (nul == s_end) break; + s = nul + 1; + } + return names; + }; + + auto A_values = get_doubles(cuopt::remote::FIELD_A_VALUES); + auto A_indices = get_ints(cuopt::remote::FIELD_A_INDICES); + auto A_offsets = get_ints(cuopt::remote::FIELD_A_OFFSETS); + if (!A_values.empty() && !A_indices.empty() && !A_offsets.empty()) { + cpu_problem.set_csr_constraint_matrix(A_values.data(), static_cast(A_values.size()), A_indices.data(), static_cast(A_indices.size()), A_offsets.data(), static_cast(A_offsets.size())); + } + + auto Q_values = get_doubles(cuopt::remote::FIELD_Q_VALUES); + auto Q_indices = get_ints(cuopt::remote::FIELD_Q_INDICES); + auto Q_offsets = get_ints(cuopt::remote::FIELD_Q_OFFSETS); + if (!Q_values.empty() && !Q_indices.empty() && !Q_offsets.empty()) { + cpu_problem.set_quadratic_objective_matrix(Q_values.data(), static_cast(Q_values.size()), Q_indices.data(), static_cast(Q_indices.size()), Q_offsets.data(), static_cast(Q_offsets.size())); + } + + auto variable_names = get_string_list(cuopt::remote::FIELD_VARIABLE_NAMES); + if (!variable_names.empty()) { cpu_problem.set_variable_names(variable_names); } + + auto row_names = get_string_list(cuopt::remote::FIELD_ROW_NAMES); + if (!row_names.empty()) { cpu_problem.set_row_names(row_names); } + + auto c = get_doubles(cuopt::remote::FIELD_C); + if (!c.empty()) { + cpu_problem.set_objective_coefficients(c.data(), static_cast(c.size())); + } + + auto b = get_doubles(cuopt::remote::FIELD_B); + if (!b.empty()) { + cpu_problem.set_constraint_bounds(b.data(), static_cast(b.size())); + } + + auto variable_lower_bounds = get_doubles(cuopt::remote::FIELD_VARIABLE_LOWER_BOUNDS); + if (!variable_lower_bounds.empty()) { + cpu_problem.set_variable_lower_bounds(variable_lower_bounds.data(), static_cast(variable_lower_bounds.size())); + } + + auto variable_upper_bounds = get_doubles(cuopt::remote::FIELD_VARIABLE_UPPER_BOUNDS); + if (!variable_upper_bounds.empty()) { + cpu_problem.set_variable_upper_bounds(variable_upper_bounds.data(), static_cast(variable_upper_bounds.size())); + } + + auto constraint_lower_bounds = get_doubles(cuopt::remote::FIELD_CONSTRAINT_LOWER_BOUNDS); + if (!constraint_lower_bounds.empty()) { + cpu_problem.set_constraint_lower_bounds(constraint_lower_bounds.data(), static_cast(constraint_lower_bounds.size())); + } + + auto constraint_upper_bounds = get_doubles(cuopt::remote::FIELD_CONSTRAINT_UPPER_BOUNDS); + if (!constraint_upper_bounds.empty()) { + cpu_problem.set_constraint_upper_bounds(constraint_upper_bounds.data(), static_cast(constraint_upper_bounds.size())); + } + + auto row_types_str = get_bytes(cuopt::remote::FIELD_ROW_TYPES); + if (!row_types_str.empty()) { + cpu_problem.set_row_types(row_types_str.data(), static_cast(row_types_str.size())); + } + + auto variable_types_ints = get_ints(cuopt::remote::FIELD_VARIABLE_TYPES); + if (!variable_types_ints.empty()) { + std::vector variable_types; + variable_types.reserve(variable_types_ints.size()); + for (auto v : variable_types_ints) { + variable_types.push_back(from_proto_variable_type(static_cast(v))); + } + cpu_problem.set_variable_types(variable_types.data(), static_cast(variable_types.size())); + } diff --git a/cpp/src/grpc/codegen/generated/generated_chunked_header_to_problem.inc b/cpp/src/grpc/codegen/generated/generated_chunked_header_to_problem.inc new file mode 100644 index 0000000000..e5ef0a3365 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_chunked_header_to_problem.inc @@ -0,0 +1,18 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + cpu_problem.set_problem_name(header.problem_name()); + cpu_problem.set_objective_name(header.objective_name()); + cpu_problem.set_maximize(header.maximize()); + cpu_problem.set_objective_scaling_factor(header.objective_scaling_factor()); + cpu_problem.set_objective_offset(header.objective_offset()); + + if (header.variable_names_size() > 0) { + std::vector variable_names(header.variable_names().begin(), header.variable_names().end()); + cpu_problem.set_variable_names(variable_names); + } + if (header.row_names_size() > 0) { + std::vector row_names(header.row_names().begin(), header.row_names().end()); + cpu_problem.set_row_names(row_names); + } diff --git a/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc b/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc new file mode 100644 index 0000000000..339093ec36 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc @@ -0,0 +1,42 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + auto primal_solution = bytes_to_typed(arrays, cuopt::remote::RESULT_PRIMAL_SOLUTION); + auto dual_solution = bytes_to_typed(arrays, cuopt::remote::RESULT_DUAL_SOLUTION); + auto reduced_cost = bytes_to_typed(arrays, cuopt::remote::RESULT_REDUCED_COST); + + auto _lp_termination_status = from_proto_pdlp_termination_status(h.lp_termination_status()); + auto _l2_primal_residual = static_cast(h.l2_primal_residual()); + auto _l2_dual_residual = static_cast(h.l2_dual_residual()); + auto _primal_objective = static_cast(h.primal_objective()); + auto _dual_objective = static_cast(h.dual_objective()); + auto _gap = static_cast(h.gap()); + auto _nb_iterations = static_cast(h.nb_iterations()); + auto _solve_time = static_cast(h.solve_time()); + auto _solved_by = static_cast(h.solved_by()); + + auto _ws_detect = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_PRIMAL_SOLUTION); + if (!_ws_detect.empty()) { + cpu_pdlp_warm_start_data_t ws; + ws.current_primal_solution_ = std::move(_ws_detect); + ws.current_dual_solution_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_DUAL_SOLUTION); + ws.initial_primal_average_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVERAGE); + ws.initial_dual_average_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_INITIAL_DUAL_AVERAGE); + ws.current_ATY_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_ATY); + ws.sum_primal_solutions_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_SUM_PRIMAL_SOLUTIONS); + ws.sum_dual_solutions_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_SUM_DUAL_SOLUTIONS); + ws.last_restart_duality_gap_primal_solution_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION); + ws.last_restart_duality_gap_dual_solution_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION); + ws.initial_primal_weight_ = static_cast(h.ws_initial_primal_weight()); + ws.initial_step_size_ = static_cast(h.ws_initial_step_size()); + ws.total_pdlp_iterations_ = static_cast(h.ws_total_pdlp_iterations()); + ws.total_pdhg_iterations_ = static_cast(h.ws_total_pdhg_iterations()); + ws.last_candidate_kkt_score_ = static_cast(h.ws_last_candidate_kkt_score()); + ws.last_restart_kkt_score_ = static_cast(h.ws_last_restart_kkt_score()); + ws.sum_solution_weight_ = static_cast(h.ws_sum_solution_weight()); + ws.iterations_since_last_restart_ = static_cast(h.ws_iterations_since_last_restart()); + return cpu_lp_solution_t(std::move(primal_solution), std::move(dual_solution), std::move(reduced_cost), _lp_termination_status, _primal_objective, _dual_objective, _solve_time, _l2_primal_residual, _l2_dual_residual, _gap, _nb_iterations, _solved_by, std::move(ws)); + } + + return cpu_lp_solution_t(std::move(primal_solution), std::move(dual_solution), std::move(reduced_cost), _lp_termination_status, _primal_objective, _dual_objective, _solve_time, _l2_primal_residual, _l2_dual_residual, _gap, _nb_iterations, _solved_by); diff --git a/cpp/src/grpc/codegen/generated/generated_chunked_to_mip_solution.inc b/cpp/src/grpc/codegen/generated/generated_chunked_to_mip_solution.inc new file mode 100644 index 0000000000..774ba6058d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_chunked_to_mip_solution.inc @@ -0,0 +1,18 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + auto mip_solution = bytes_to_typed(arrays, cuopt::remote::RESULT_MIP_SOLUTION); + + auto _mip_termination_status = from_proto_mip_termination_status(h.mip_termination_status()); + auto _mip_objective = static_cast(h.mip_objective()); + auto _mip_gap = static_cast(h.mip_gap()); + auto _solution_bound = static_cast(h.solution_bound()); + auto _total_solve_time = static_cast(h.total_solve_time()); + auto _presolve_time = static_cast(h.presolve_time()); + auto _max_constraint_violation = static_cast(h.max_constraint_violation()); + auto _max_int_violation = static_cast(h.max_int_violation()); + auto _max_variable_bound_violation = static_cast(h.max_variable_bound_violation()); + auto _nodes = static_cast(h.nodes()); + auto _simplex_iterations = static_cast(h.simplex_iterations()); + return cpu_mip_solution_t(std::move(mip_solution), _mip_termination_status, _mip_objective, _mip_gap, _solution_bound, _total_solve_time, _presolve_time, _max_constraint_violation, _max_int_violation, _max_variable_bound_violation, _nodes, _simplex_iterations); diff --git a/cpp/src/grpc/codegen/generated/generated_collect_lp_arrays.inc b/cpp/src/grpc/codegen/generated/generated_collect_lp_arrays.inc new file mode 100644 index 0000000000..e4c4a9d286 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_collect_lp_arrays.inc @@ -0,0 +1,24 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + std::map> arrays; + const auto& _primal_solution = solution.get_primal_solution_host(); + if (!_primal_solution.empty()) { arrays[cuopt::remote::RESULT_PRIMAL_SOLUTION] = doubles_to_bytes(_primal_solution); } + const auto& _dual_solution = solution.get_dual_solution_host(); + if (!_dual_solution.empty()) { arrays[cuopt::remote::RESULT_DUAL_SOLUTION] = doubles_to_bytes(_dual_solution); } + const auto& _reduced_cost = solution.get_reduced_cost_host(); + if (!_reduced_cost.empty()) { arrays[cuopt::remote::RESULT_REDUCED_COST] = doubles_to_bytes(_reduced_cost); } + if (solution.has_warm_start_data()) { + const auto& ws = solution.get_cpu_pdlp_warm_start_data(); + if (!ws.current_primal_solution_.empty()) { arrays[cuopt::remote::RESULT_WS_CURRENT_PRIMAL_SOLUTION] = doubles_to_bytes(ws.current_primal_solution_); } + if (!ws.current_dual_solution_.empty()) { arrays[cuopt::remote::RESULT_WS_CURRENT_DUAL_SOLUTION] = doubles_to_bytes(ws.current_dual_solution_); } + if (!ws.initial_primal_average_.empty()) { arrays[cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVERAGE] = doubles_to_bytes(ws.initial_primal_average_); } + if (!ws.initial_dual_average_.empty()) { arrays[cuopt::remote::RESULT_WS_INITIAL_DUAL_AVERAGE] = doubles_to_bytes(ws.initial_dual_average_); } + if (!ws.current_ATY_.empty()) { arrays[cuopt::remote::RESULT_WS_CURRENT_ATY] = doubles_to_bytes(ws.current_ATY_); } + if (!ws.sum_primal_solutions_.empty()) { arrays[cuopt::remote::RESULT_WS_SUM_PRIMAL_SOLUTIONS] = doubles_to_bytes(ws.sum_primal_solutions_); } + if (!ws.sum_dual_solutions_.empty()) { arrays[cuopt::remote::RESULT_WS_SUM_DUAL_SOLUTIONS] = doubles_to_bytes(ws.sum_dual_solutions_); } + if (!ws.last_restart_duality_gap_primal_solution_.empty()) { arrays[cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION] = doubles_to_bytes(ws.last_restart_duality_gap_primal_solution_); } + if (!ws.last_restart_duality_gap_dual_solution_.empty()) { arrays[cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION] = doubles_to_bytes(ws.last_restart_duality_gap_dual_solution_); } + } + return arrays; diff --git a/cpp/src/grpc/codegen/generated/generated_collect_mip_arrays.inc b/cpp/src/grpc/codegen/generated/generated_collect_mip_arrays.inc new file mode 100644 index 0000000000..f2961850a5 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_collect_mip_arrays.inc @@ -0,0 +1,8 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + std::map> arrays; + const auto& _mip_solution = solution.get_solution_host(); + if (!_mip_solution.empty()) { arrays[cuopt::remote::RESULT_MIP_SOLUTION] = doubles_to_bytes(_mip_solution); } + return arrays; diff --git a/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc b/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc new file mode 100644 index 0000000000..6cb61f3b8d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc @@ -0,0 +1,39 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ +cuopt::remote::VariableType to_proto_variable_type(var_t v) +{ + switch (v) { + case var_t::CONTINUOUS: return cuopt::remote::CONTINUOUS; + case var_t::INTEGER: return cuopt::remote::INTEGER; + default: return cuopt::remote::CONTINUOUS; + } +} + +var_t from_proto_variable_type(cuopt::remote::VariableType v) +{ + switch (v) { + case cuopt::remote::CONTINUOUS: return var_t::CONTINUOUS; + case cuopt::remote::INTEGER: return var_t::INTEGER; + default: return var_t::CONTINUOUS; + } +} + +cuopt::remote::ProblemCategory to_proto_problem_category(problem_category_t v) +{ + switch (v) { + case problem_category_t::LP: return cuopt::remote::LP; + case problem_category_t::MIP: return cuopt::remote::MIP; + default: return cuopt::remote::LP; + } +} + +problem_category_t from_proto_problem_category(cuopt::remote::ProblemCategory v) +{ + switch (v) { + case cuopt::remote::LP: return problem_category_t::LP; + case cuopt::remote::MIP: return problem_category_t::MIP; + default: return problem_category_t::LP; + } +} diff --git a/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc b/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc new file mode 100644 index 0000000000..cd4dad1190 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc @@ -0,0 +1,49 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ +cuopt::remote::PDLPSolverMode to_proto_pdlp_solver_mode(pdlp_solver_mode_t v) +{ + switch (v) { + case pdlp_solver_mode_t::Stable1: return cuopt::remote::Stable1; + case pdlp_solver_mode_t::Stable2: return cuopt::remote::Stable2; + case pdlp_solver_mode_t::Methodical1: return cuopt::remote::Methodical1; + case pdlp_solver_mode_t::Fast1: return cuopt::remote::Fast1; + case pdlp_solver_mode_t::Stable3: return cuopt::remote::Stable3; + default: return cuopt::remote::Stable3; + } +} + +pdlp_solver_mode_t from_proto_pdlp_solver_mode(cuopt::remote::PDLPSolverMode v) +{ + switch (v) { + case cuopt::remote::Stable1: return pdlp_solver_mode_t::Stable1; + case cuopt::remote::Stable2: return pdlp_solver_mode_t::Stable2; + case cuopt::remote::Methodical1: return pdlp_solver_mode_t::Methodical1; + case cuopt::remote::Fast1: return pdlp_solver_mode_t::Fast1; + case cuopt::remote::Stable3: return pdlp_solver_mode_t::Stable3; + default: return pdlp_solver_mode_t::Stable3; + } +} + +cuopt::remote::LPMethod to_proto_lp_method(method_t v) +{ + switch (v) { + case method_t::Concurrent: return cuopt::remote::Concurrent; + case method_t::PDLP: return cuopt::remote::PDLP; + case method_t::DualSimplex: return cuopt::remote::DualSimplex; + case method_t::Barrier: return cuopt::remote::Barrier; + default: return cuopt::remote::Concurrent; + } +} + +method_t from_proto_lp_method(cuopt::remote::LPMethod v) +{ + switch (v) { + case cuopt::remote::Concurrent: return method_t::Concurrent; + case cuopt::remote::PDLP: return method_t::PDLP; + case cuopt::remote::DualSimplex: return method_t::DualSimplex; + case cuopt::remote::Barrier: return method_t::Barrier; + default: return method_t::Concurrent; + } +} diff --git a/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc b/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc new file mode 100644 index 0000000000..725e808a55 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc @@ -0,0 +1,63 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ +cuopt::remote::PDLPTerminationStatus to_proto_pdlp_termination_status(pdlp_termination_status_t v) +{ + switch (v) { + case pdlp_termination_status_t::NoTermination: return cuopt::remote::PDLP_NO_TERMINATION; + case pdlp_termination_status_t::NumericalError: return cuopt::remote::PDLP_NUMERICAL_ERROR; + case pdlp_termination_status_t::Optimal: return cuopt::remote::PDLP_OPTIMAL; + case pdlp_termination_status_t::PrimalInfeasible: return cuopt::remote::PDLP_PRIMAL_INFEASIBLE; + case pdlp_termination_status_t::DualInfeasible: return cuopt::remote::PDLP_DUAL_INFEASIBLE; + case pdlp_termination_status_t::IterationLimit: return cuopt::remote::PDLP_ITERATION_LIMIT; + case pdlp_termination_status_t::TimeLimit: return cuopt::remote::PDLP_TIME_LIMIT; + case pdlp_termination_status_t::ConcurrentLimit: return cuopt::remote::PDLP_CONCURRENT_LIMIT; + case pdlp_termination_status_t::PrimalFeasible: return cuopt::remote::PDLP_PRIMAL_FEASIBLE; + default: return cuopt::remote::PDLP_NO_TERMINATION; + } +} + +pdlp_termination_status_t from_proto_pdlp_termination_status(cuopt::remote::PDLPTerminationStatus v) +{ + switch (v) { + case cuopt::remote::PDLP_NO_TERMINATION: return pdlp_termination_status_t::NoTermination; + case cuopt::remote::PDLP_NUMERICAL_ERROR: return pdlp_termination_status_t::NumericalError; + case cuopt::remote::PDLP_OPTIMAL: return pdlp_termination_status_t::Optimal; + case cuopt::remote::PDLP_PRIMAL_INFEASIBLE: return pdlp_termination_status_t::PrimalInfeasible; + case cuopt::remote::PDLP_DUAL_INFEASIBLE: return pdlp_termination_status_t::DualInfeasible; + case cuopt::remote::PDLP_ITERATION_LIMIT: return pdlp_termination_status_t::IterationLimit; + case cuopt::remote::PDLP_TIME_LIMIT: return pdlp_termination_status_t::TimeLimit; + case cuopt::remote::PDLP_CONCURRENT_LIMIT: return pdlp_termination_status_t::ConcurrentLimit; + case cuopt::remote::PDLP_PRIMAL_FEASIBLE: return pdlp_termination_status_t::PrimalFeasible; + default: return pdlp_termination_status_t::NoTermination; + } +} + +cuopt::remote::MIPTerminationStatus to_proto_mip_termination_status(mip_termination_status_t v) +{ + switch (v) { + case mip_termination_status_t::NoTermination: return cuopt::remote::MIP_NO_TERMINATION; + case mip_termination_status_t::Optimal: return cuopt::remote::MIP_OPTIMAL; + case mip_termination_status_t::FeasibleFound: return cuopt::remote::MIP_FEASIBLE_FOUND; + case mip_termination_status_t::Infeasible: return cuopt::remote::MIP_INFEASIBLE; + case mip_termination_status_t::Unbounded: return cuopt::remote::MIP_UNBOUNDED; + case mip_termination_status_t::TimeLimit: return cuopt::remote::MIP_TIME_LIMIT; + case mip_termination_status_t::WorkLimit: return cuopt::remote::MIP_WORK_LIMIT; + default: return cuopt::remote::MIP_NO_TERMINATION; + } +} + +mip_termination_status_t from_proto_mip_termination_status(cuopt::remote::MIPTerminationStatus v) +{ + switch (v) { + case cuopt::remote::MIP_NO_TERMINATION: return mip_termination_status_t::NoTermination; + case cuopt::remote::MIP_OPTIMAL: return mip_termination_status_t::Optimal; + case cuopt::remote::MIP_FEASIBLE_FOUND: return mip_termination_status_t::FeasibleFound; + case cuopt::remote::MIP_INFEASIBLE: return mip_termination_status_t::Infeasible; + case cuopt::remote::MIP_UNBOUNDED: return mip_termination_status_t::Unbounded; + case cuopt::remote::MIP_TIME_LIMIT: return mip_termination_status_t::TimeLimit; + case cuopt::remote::MIP_WORK_LIMIT: return mip_termination_status_t::WorkLimit; + default: return mip_termination_status_t::NoTermination; + } +} diff --git a/cpp/src/grpc/codegen/generated/generated_estimate_lp_size.inc b/cpp/src/grpc/codegen/generated/generated_estimate_lp_size.inc new file mode 100644 index 0000000000..b170d7f07e --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_estimate_lp_size.inc @@ -0,0 +1,22 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + size_t est = 0; + est += static_cast(solution.get_primal_solution_size()) * sizeof(double); + est += static_cast(solution.get_dual_solution_size()) * sizeof(double); + est += static_cast(solution.get_reduced_cost_size()) * sizeof(double); + if (solution.has_warm_start_data()) { + const auto& ws = solution.get_cpu_pdlp_warm_start_data(); + est += ws.current_primal_solution_.size() * sizeof(double); + est += ws.current_dual_solution_.size() * sizeof(double); + est += ws.initial_primal_average_.size() * sizeof(double); + est += ws.initial_dual_average_.size() * sizeof(double); + est += ws.current_ATY_.size() * sizeof(double); + est += ws.sum_primal_solutions_.size() * sizeof(double); + est += ws.sum_dual_solutions_.size() * sizeof(double); + est += ws.last_restart_duality_gap_primal_solution_.size() * sizeof(double); + est += ws.last_restart_duality_gap_dual_solution_.size() * sizeof(double); + } + est += 512; + return est; diff --git a/cpp/src/grpc/codegen/generated/generated_estimate_mip_size.inc b/cpp/src/grpc/codegen/generated/generated_estimate_mip_size.inc new file mode 100644 index 0000000000..08c374c300 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_estimate_mip_size.inc @@ -0,0 +1,8 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + size_t est = 0; + est += static_cast(solution.get_solution_size()) * sizeof(double); + est += 256; + return est; diff --git a/cpp/src/grpc/codegen/generated/generated_estimate_problem_size.inc b/cpp/src/grpc/codegen/generated/generated_estimate_problem_size.inc new file mode 100644 index 0000000000..d156849701 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_estimate_problem_size.inc @@ -0,0 +1,23 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + size_t est = 0; + for (const auto& s : cpu_problem.get_variable_names()) est += s.size() + 2; + for (const auto& s : cpu_problem.get_row_names()) est += s.size() + 2; + est += cpu_problem.get_constraint_matrix_values_host().size() * sizeof(double); + est += cpu_problem.get_constraint_matrix_indices_host().size() * 5; + est += cpu_problem.get_constraint_matrix_offsets_host().size() * 5; + est += cpu_problem.get_objective_coefficients_host().size() * sizeof(double); + est += cpu_problem.get_constraint_bounds_host().size() * sizeof(double); + est += cpu_problem.get_variable_lower_bounds_host().size() * sizeof(double); + est += cpu_problem.get_variable_upper_bounds_host().size() * sizeof(double); + est += cpu_problem.get_constraint_lower_bounds_host().size() * sizeof(double); + est += cpu_problem.get_constraint_upper_bounds_host().size() * sizeof(double); + est += cpu_problem.get_row_types_host().size(); + est += cpu_problem.get_variable_types_host().size() * 4; + est += cpu_problem.get_quadratic_objective_values_host().size() * sizeof(double); + est += cpu_problem.get_quadratic_objective_indices_host().size() * 5; + est += cpu_problem.get_quadratic_objective_offsets_host().size() * 5; + est += 512; + return est; diff --git a/cpp/src/grpc/codegen/generated/generated_lp_chunked_header.inc b/cpp/src/grpc/codegen/generated/generated_lp_chunked_header.inc new file mode 100644 index 0000000000..a514f7c57d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_lp_chunked_header.inc @@ -0,0 +1,41 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + header->set_problem_category(cuopt::remote::LP); + header->set_lp_termination_status(to_proto_pdlp_termination_status(solution.get_termination_status())); + header->set_error_message(solution.get_error_status().what()); + header->set_l2_primal_residual(solution.get_l2_primal_residual()); + header->set_l2_dual_residual(solution.get_l2_dual_residual()); + header->set_primal_objective(solution.get_objective_value()); + header->set_dual_objective(solution.get_dual_objective_value()); + header->set_gap(solution.get_gap()); + header->set_nb_iterations(solution.get_num_iterations()); + header->set_solve_time(solution.get_solve_time()); + header->set_solved_by(static_cast(solution.solved_by())); + + add_result_array_descriptor(header, cuopt::remote::RESULT_PRIMAL_SOLUTION, solution.get_primal_solution_host().size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_DUAL_SOLUTION, solution.get_dual_solution_host().size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_REDUCED_COST, solution.get_reduced_cost_host().size(), sizeof(double)); + + if (solution.has_warm_start_data()) { + const auto& ws = solution.get_cpu_pdlp_warm_start_data(); + header->set_ws_initial_primal_weight(static_cast(ws.initial_primal_weight_)); + header->set_ws_initial_step_size(static_cast(ws.initial_step_size_)); + header->set_ws_total_pdlp_iterations(static_cast(ws.total_pdlp_iterations_)); + header->set_ws_total_pdhg_iterations(static_cast(ws.total_pdhg_iterations_)); + header->set_ws_last_candidate_kkt_score(static_cast(ws.last_candidate_kkt_score_)); + header->set_ws_last_restart_kkt_score(static_cast(ws.last_restart_kkt_score_)); + header->set_ws_sum_solution_weight(static_cast(ws.sum_solution_weight_)); + header->set_ws_iterations_since_last_restart(static_cast(ws.iterations_since_last_restart_)); + + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_CURRENT_PRIMAL_SOLUTION, ws.current_primal_solution_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_CURRENT_DUAL_SOLUTION, ws.current_dual_solution_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVERAGE, ws.initial_primal_average_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_INITIAL_DUAL_AVERAGE, ws.initial_dual_average_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_CURRENT_ATY, ws.current_ATY_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_SUM_PRIMAL_SOLUTIONS, ws.sum_primal_solutions_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_SUM_DUAL_SOLUTIONS, ws.sum_dual_solutions_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION, ws.last_restart_duality_gap_primal_solution_.size(), sizeof(double)); + add_result_array_descriptor(header, cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION, ws.last_restart_duality_gap_dual_solution_.size(), sizeof(double)); + } diff --git a/cpp/src/grpc/codegen/generated/generated_lp_solution_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_lp_solution_to_proto.inc new file mode 100644 index 0000000000..d457ae87f6 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_lp_solution_to_proto.inc @@ -0,0 +1,43 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + pb_solution->set_lp_termination_status(to_proto_pdlp_termination_status(solution.get_termination_status())); + pb_solution->set_error_message(solution.get_error_status().what()); + pb_solution->set_l2_primal_residual(solution.get_l2_primal_residual()); + pb_solution->set_l2_dual_residual(solution.get_l2_dual_residual()); + pb_solution->set_primal_objective(solution.get_objective_value()); + pb_solution->set_dual_objective(solution.get_dual_objective_value()); + pb_solution->set_gap(solution.get_gap()); + pb_solution->set_nb_iterations(solution.get_num_iterations()); + pb_solution->set_solve_time(solution.get_solve_time()); + pb_solution->set_solved_by(static_cast(solution.solved_by())); + + const auto& _primal_solution = solution.get_primal_solution_host(); + for (const auto& v : _primal_solution) pb_solution->add_primal_solution(static_cast(v)); + const auto& _dual_solution = solution.get_dual_solution_host(); + for (const auto& v : _dual_solution) pb_solution->add_dual_solution(static_cast(v)); + const auto& _reduced_cost = solution.get_reduced_cost_host(); + for (const auto& v : _reduced_cost) pb_solution->add_reduced_cost(static_cast(v)); + + if (solution.has_warm_start_data()) { + auto* pb_ws = pb_solution->mutable_warm_start_data(); + const auto& ws = solution.get_cpu_pdlp_warm_start_data(); + for (const auto& v : ws.current_primal_solution_) pb_ws->add_current_primal_solution(static_cast(v)); + for (const auto& v : ws.current_dual_solution_) pb_ws->add_current_dual_solution(static_cast(v)); + for (const auto& v : ws.initial_primal_average_) pb_ws->add_initial_primal_average(static_cast(v)); + for (const auto& v : ws.initial_dual_average_) pb_ws->add_initial_dual_average(static_cast(v)); + for (const auto& v : ws.current_ATY_) pb_ws->add_current_aty(static_cast(v)); + for (const auto& v : ws.sum_primal_solutions_) pb_ws->add_sum_primal_solutions(static_cast(v)); + for (const auto& v : ws.sum_dual_solutions_) pb_ws->add_sum_dual_solutions(static_cast(v)); + for (const auto& v : ws.last_restart_duality_gap_primal_solution_) pb_ws->add_last_restart_duality_gap_primal_solution(static_cast(v)); + for (const auto& v : ws.last_restart_duality_gap_dual_solution_) pb_ws->add_last_restart_duality_gap_dual_solution(static_cast(v)); + pb_ws->set_initial_primal_weight(static_cast(ws.initial_primal_weight_)); + pb_ws->set_initial_step_size(static_cast(ws.initial_step_size_)); + pb_ws->set_total_pdlp_iterations(static_cast(ws.total_pdlp_iterations_)); + pb_ws->set_total_pdhg_iterations(static_cast(ws.total_pdhg_iterations_)); + pb_ws->set_last_candidate_kkt_score(static_cast(ws.last_candidate_kkt_score_)); + pb_ws->set_last_restart_kkt_score(static_cast(ws.last_restart_kkt_score_)); + pb_ws->set_sum_solution_weight(static_cast(ws.sum_solution_weight_)); + pb_ws->set_iterations_since_last_restart(static_cast(ws.iterations_since_last_restart_)); + } diff --git a/cpp/src/grpc/codegen/generated/generated_mip_chunked_header.inc b/cpp/src/grpc/codegen/generated/generated_mip_chunked_header.inc new file mode 100644 index 0000000000..459bd7736e --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_mip_chunked_header.inc @@ -0,0 +1,19 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + header->set_problem_category(cuopt::remote::MIP); + header->set_mip_termination_status(to_proto_mip_termination_status(solution.get_termination_status())); + header->set_mip_error_message(solution.get_error_status().what()); + header->set_mip_objective(solution.get_objective_value()); + header->set_mip_gap(solution.get_mip_gap()); + header->set_solution_bound(solution.get_solution_bound()); + header->set_total_solve_time(solution.get_solve_time()); + header->set_presolve_time(solution.get_presolve_time()); + header->set_max_constraint_violation(solution.get_max_constraint_violation()); + header->set_max_int_violation(solution.get_max_int_violation()); + header->set_max_variable_bound_violation(solution.get_max_variable_bound_violation()); + header->set_nodes(solution.get_num_nodes()); + header->set_simplex_iterations(solution.get_num_simplex_iterations()); + + add_result_array_descriptor(header, cuopt::remote::RESULT_MIP_SOLUTION, solution.get_solution_host().size(), sizeof(double)); diff --git a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc new file mode 100644 index 0000000000..e5b4d1af9d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc @@ -0,0 +1,39 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + pb_settings->set_time_limit(settings.time_limit); + pb_settings->set_relative_mip_gap(settings.tolerances.relative_mip_gap); + pb_settings->set_absolute_mip_gap(settings.tolerances.absolute_mip_gap); + pb_settings->set_integrality_tolerance(settings.tolerances.integrality_tolerance); + pb_settings->set_absolute_tolerance(settings.tolerances.absolute_tolerance); + pb_settings->set_relative_tolerance(settings.tolerances.relative_tolerance); + pb_settings->set_presolve_absolute_tolerance(settings.tolerances.presolve_absolute_tolerance); + pb_settings->set_log_to_console(settings.log_to_console); + pb_settings->set_heuristics_only(settings.heuristics_only); + pb_settings->set_num_cpu_threads(settings.num_cpu_threads); + pb_settings->set_num_gpus(settings.num_gpus); + pb_settings->set_presolver(static_cast(settings.presolver)); + pb_settings->set_mip_scaling(settings.mip_scaling); + pb_settings->set_work_limit(settings.work_limit); + if (settings.node_limit == std::numeric_limits::max()) { + pb_settings->set_node_limit(-1); + } else { + pb_settings->set_node_limit(settings.node_limit); + } + pb_settings->set_reliability_branching(settings.reliability_branching); + pb_settings->set_mip_batch_pdlp_strong_branching(settings.mip_batch_pdlp_strong_branching); + pb_settings->set_mip_batch_pdlp_reliability_branching(settings.mip_batch_pdlp_reliability_branching); + pb_settings->set_strong_branching_simplex_iteration_limit(settings.strong_branching_simplex_iteration_limit); + pb_settings->set_max_cut_passes(settings.max_cut_passes); + pb_settings->set_mir_cuts(settings.mir_cuts); + pb_settings->set_mixed_integer_gomory_cuts(settings.mixed_integer_gomory_cuts); + pb_settings->set_knapsack_cuts(settings.knapsack_cuts); + pb_settings->set_clique_cuts(settings.clique_cuts); + pb_settings->set_implied_bound_cuts(settings.implied_bound_cuts); + pb_settings->set_strong_chvatal_gomory_cuts(settings.strong_chvatal_gomory_cuts); + pb_settings->set_reduced_cost_strengthening(settings.reduced_cost_strengthening); + pb_settings->set_cut_change_threshold(settings.cut_change_threshold); + pb_settings->set_cut_min_orthogonality(settings.cut_min_orthogonality); + pb_settings->set_determinism_mode(settings.determinism_mode); + pb_settings->set_seed(settings.seed); diff --git a/cpp/src/grpc/codegen/generated/generated_mip_solution_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_mip_solution_to_proto.inc new file mode 100644 index 0000000000..b04002e758 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_mip_solution_to_proto.inc @@ -0,0 +1,19 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + pb_solution->set_mip_termination_status(to_proto_mip_termination_status(solution.get_termination_status())); + pb_solution->set_mip_error_message(solution.get_error_status().what()); + pb_solution->set_mip_objective(solution.get_objective_value()); + pb_solution->set_mip_gap(solution.get_mip_gap()); + pb_solution->set_solution_bound(solution.get_solution_bound()); + pb_solution->set_total_solve_time(solution.get_solve_time()); + pb_solution->set_presolve_time(solution.get_presolve_time()); + pb_solution->set_max_constraint_violation(solution.get_max_constraint_violation()); + pb_solution->set_max_int_violation(solution.get_max_int_violation()); + pb_solution->set_max_variable_bound_violation(solution.get_max_variable_bound_violation()); + pb_solution->set_nodes(solution.get_num_nodes()); + pb_solution->set_simplex_iterations(solution.get_num_simplex_iterations()); + + const auto& _mip_solution = solution.get_solution_host(); + for (const auto& v : _mip_solution) pb_solution->add_mip_solution(static_cast(v)); diff --git a/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc new file mode 100644 index 0000000000..fbd863fd6d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc @@ -0,0 +1,38 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + pb_settings->set_absolute_gap_tolerance(settings.tolerances.absolute_gap_tolerance); + pb_settings->set_relative_gap_tolerance(settings.tolerances.relative_gap_tolerance); + pb_settings->set_primal_infeasible_tolerance(settings.tolerances.primal_infeasible_tolerance); + pb_settings->set_dual_infeasible_tolerance(settings.tolerances.dual_infeasible_tolerance); + pb_settings->set_absolute_dual_tolerance(settings.tolerances.absolute_dual_tolerance); + pb_settings->set_relative_dual_tolerance(settings.tolerances.relative_dual_tolerance); + pb_settings->set_absolute_primal_tolerance(settings.tolerances.absolute_primal_tolerance); + pb_settings->set_relative_primal_tolerance(settings.tolerances.relative_primal_tolerance); + pb_settings->set_time_limit(settings.time_limit); + if (settings.iteration_limit == std::numeric_limits::max()) { + pb_settings->set_iteration_limit(-1); + } else { + pb_settings->set_iteration_limit(static_cast(settings.iteration_limit)); + } + pb_settings->set_log_to_console(settings.log_to_console); + pb_settings->set_detect_infeasibility(settings.detect_infeasibility); + pb_settings->set_strict_infeasibility(settings.strict_infeasibility); + pb_settings->set_pdlp_solver_mode(to_proto_pdlp_solver_mode(settings.pdlp_solver_mode)); + pb_settings->set_method(to_proto_lp_method(settings.method)); + pb_settings->set_presolver(static_cast(settings.presolver)); + pb_settings->set_dual_postsolve(settings.dual_postsolve); + pb_settings->set_crossover(settings.crossover); + pb_settings->set_num_gpus(settings.num_gpus); + pb_settings->set_per_constraint_residual(settings.per_constraint_residual); + pb_settings->set_cudss_deterministic(settings.cudss_deterministic); + pb_settings->set_folding(settings.folding); + pb_settings->set_augmented(settings.augmented); + pb_settings->set_dualize(settings.dualize); + pb_settings->set_ordering(settings.ordering); + pb_settings->set_barrier_dual_initial_point(settings.barrier_dual_initial_point); + pb_settings->set_eliminate_dense_columns(settings.eliminate_dense_columns); + pb_settings->set_save_best_primal_so_far(settings.save_best_primal_so_far); + pb_settings->set_first_primal_feasible(settings.first_primal_feasible); + pb_settings->set_pdlp_precision(static_cast(settings.pdlp_precision)); diff --git a/cpp/src/grpc/codegen/generated/generated_populate_chunked_header_lp.inc b/cpp/src/grpc/codegen/generated/generated_populate_chunked_header_lp.inc new file mode 100644 index 0000000000..2f02d07b60 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_populate_chunked_header_lp.inc @@ -0,0 +1,15 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + auto* rh = header->mutable_header(); + rh->set_version(1); + rh->set_problem_category(cuopt::remote::LP); + + header->set_problem_name(cpu_problem.get_problem_name()); + header->set_objective_name(cpu_problem.get_objective_name()); + header->set_maximize(cpu_problem.get_sense()); + header->set_objective_scaling_factor(cpu_problem.get_objective_scaling_factor()); + header->set_objective_offset(cpu_problem.get_objective_offset()); + + map_pdlp_settings_to_proto(settings, header->mutable_lp_settings()); diff --git a/cpp/src/grpc/codegen/generated/generated_populate_chunked_header_mip.inc b/cpp/src/grpc/codegen/generated/generated_populate_chunked_header_mip.inc new file mode 100644 index 0000000000..8ab7ca6e70 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_populate_chunked_header_mip.inc @@ -0,0 +1,16 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + auto* rh = header->mutable_header(); + rh->set_version(1); + rh->set_problem_category(cuopt::remote::MIP); + + header->set_problem_name(cpu_problem.get_problem_name()); + header->set_objective_name(cpu_problem.get_objective_name()); + header->set_maximize(cpu_problem.get_sense()); + header->set_objective_scaling_factor(cpu_problem.get_objective_scaling_factor()); + header->set_objective_offset(cpu_problem.get_objective_offset()); + + map_mip_settings_to_proto(settings, header->mutable_mip_settings()); + header->set_enable_incumbents(enable_incumbents); diff --git a/cpp/src/grpc/codegen/generated/generated_problem_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_problem_to_proto.inc new file mode 100644 index 0000000000..d587c0e12e --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_problem_to_proto.inc @@ -0,0 +1,70 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + pb_problem->set_problem_name(cpu_problem.get_problem_name()); + pb_problem->set_objective_name(cpu_problem.get_objective_name()); + pb_problem->set_maximize(cpu_problem.get_sense()); + pb_problem->set_objective_scaling_factor(cpu_problem.get_objective_scaling_factor()); + pb_problem->set_objective_offset(cpu_problem.get_objective_offset()); + + for (const auto& s : cpu_problem.get_variable_names()) pb_problem->add_variable_names(s); + for (const auto& s : cpu_problem.get_row_names()) pb_problem->add_row_names(s); + { + auto _c = cpu_problem.get_objective_coefficients_host(); + for (const auto& v : _c) pb_problem->add_c(static_cast(v)); + } + { + auto _b = cpu_problem.get_constraint_bounds_host(); + for (const auto& v : _b) pb_problem->add_b(static_cast(v)); + } + { + auto _variable_lower_bounds = cpu_problem.get_variable_lower_bounds_host(); + for (const auto& v : _variable_lower_bounds) pb_problem->add_variable_lower_bounds(static_cast(v)); + } + { + auto _variable_upper_bounds = cpu_problem.get_variable_upper_bounds_host(); + for (const auto& v : _variable_upper_bounds) pb_problem->add_variable_upper_bounds(static_cast(v)); + } + { + auto _constraint_lower_bounds = cpu_problem.get_constraint_lower_bounds_host(); + for (const auto& v : _constraint_lower_bounds) pb_problem->add_constraint_lower_bounds(static_cast(v)); + } + { + auto _constraint_upper_bounds = cpu_problem.get_constraint_upper_bounds_host(); + for (const auto& v : _constraint_upper_bounds) pb_problem->add_constraint_upper_bounds(static_cast(v)); + } + { + auto _row_types = cpu_problem.get_row_types_host(); + if (!_row_types.empty()) { + pb_problem->set_row_types(std::string(_row_types.begin(), _row_types.end())); + } + } + { + auto _variable_types = cpu_problem.get_variable_types_host(); + for (const auto& v : _variable_types) pb_problem->add_variable_types(to_proto_variable_type(v)); + } + { + auto _A_values = cpu_problem.get_constraint_matrix_values_host(); + for (const auto& v : _A_values) pb_problem->add_a_values(static_cast(v)); + } + { + auto _A_indices = cpu_problem.get_constraint_matrix_indices_host(); + for (const auto& v : _A_indices) pb_problem->add_a_indices(static_cast(v)); + } + { + auto _A_offsets = cpu_problem.get_constraint_matrix_offsets_host(); + for (const auto& v : _A_offsets) pb_problem->add_a_offsets(static_cast(v)); + } + { + auto _Q_values = cpu_problem.get_quadratic_objective_values_host(); + for (const auto& v : _Q_values) pb_problem->add_q_values(static_cast(v)); + } + { + auto _Q_indices = cpu_problem.get_quadratic_objective_indices_host(); + for (const auto& v : _Q_indices) pb_problem->add_q_indices(static_cast(v)); + } + { + auto _Q_offsets = cpu_problem.get_quadratic_objective_offsets_host(); + for (const auto& v : _Q_offsets) pb_problem->add_q_offsets(static_cast(v)); + } diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_lp_solution.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_lp_solution.inc new file mode 100644 index 0000000000..d586f60425 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_lp_solution.inc @@ -0,0 +1,42 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + std::vector primal_solution(pb_solution.primal_solution().begin(), pb_solution.primal_solution().end()); + std::vector dual_solution(pb_solution.dual_solution().begin(), pb_solution.dual_solution().end()); + std::vector reduced_cost(pb_solution.reduced_cost().begin(), pb_solution.reduced_cost().end()); + + auto _lp_termination_status = from_proto_pdlp_termination_status(pb_solution.lp_termination_status()); + auto _l2_primal_residual = static_cast(pb_solution.l2_primal_residual()); + auto _l2_dual_residual = static_cast(pb_solution.l2_dual_residual()); + auto _primal_objective = static_cast(pb_solution.primal_objective()); + auto _dual_objective = static_cast(pb_solution.dual_objective()); + auto _gap = static_cast(pb_solution.gap()); + auto _nb_iterations = static_cast(pb_solution.nb_iterations()); + auto _solve_time = static_cast(pb_solution.solve_time()); + auto _solved_by = static_cast(pb_solution.solved_by()); + + if (pb_solution.has_warm_start_data()) { + const auto& pb_ws = pb_solution.warm_start_data(); + cpu_pdlp_warm_start_data_t ws; + ws.current_primal_solution_.assign(pb_ws.current_primal_solution().begin(), pb_ws.current_primal_solution().end()); + ws.current_dual_solution_.assign(pb_ws.current_dual_solution().begin(), pb_ws.current_dual_solution().end()); + ws.initial_primal_average_.assign(pb_ws.initial_primal_average().begin(), pb_ws.initial_primal_average().end()); + ws.initial_dual_average_.assign(pb_ws.initial_dual_average().begin(), pb_ws.initial_dual_average().end()); + ws.current_ATY_.assign(pb_ws.current_aty().begin(), pb_ws.current_aty().end()); + ws.sum_primal_solutions_.assign(pb_ws.sum_primal_solutions().begin(), pb_ws.sum_primal_solutions().end()); + ws.sum_dual_solutions_.assign(pb_ws.sum_dual_solutions().begin(), pb_ws.sum_dual_solutions().end()); + ws.last_restart_duality_gap_primal_solution_.assign(pb_ws.last_restart_duality_gap_primal_solution().begin(), pb_ws.last_restart_duality_gap_primal_solution().end()); + ws.last_restart_duality_gap_dual_solution_.assign(pb_ws.last_restart_duality_gap_dual_solution().begin(), pb_ws.last_restart_duality_gap_dual_solution().end()); + ws.initial_primal_weight_ = static_cast(pb_ws.initial_primal_weight()); + ws.initial_step_size_ = static_cast(pb_ws.initial_step_size()); + ws.total_pdlp_iterations_ = static_cast(pb_ws.total_pdlp_iterations()); + ws.total_pdhg_iterations_ = static_cast(pb_ws.total_pdhg_iterations()); + ws.last_candidate_kkt_score_ = static_cast(pb_ws.last_candidate_kkt_score()); + ws.last_restart_kkt_score_ = static_cast(pb_ws.last_restart_kkt_score()); + ws.sum_solution_weight_ = static_cast(pb_ws.sum_solution_weight()); + ws.iterations_since_last_restart_ = static_cast(pb_ws.iterations_since_last_restart()); + return cpu_lp_solution_t(std::move(primal_solution), std::move(dual_solution), std::move(reduced_cost), _lp_termination_status, _primal_objective, _dual_objective, _solve_time, _l2_primal_residual, _l2_dual_residual, _gap, _nb_iterations, _solved_by, std::move(ws)); + } + + return cpu_lp_solution_t(std::move(primal_solution), std::move(dual_solution), std::move(reduced_cost), _lp_termination_status, _primal_objective, _dual_objective, _solve_time, _l2_primal_residual, _l2_dual_residual, _gap, _nb_iterations, _solved_by); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc new file mode 100644 index 0000000000..87a663e5c9 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc @@ -0,0 +1,37 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + settings.time_limit = pb_settings.time_limit(); + settings.tolerances.relative_mip_gap = pb_settings.relative_mip_gap(); + settings.tolerances.absolute_mip_gap = pb_settings.absolute_mip_gap(); + settings.tolerances.integrality_tolerance = pb_settings.integrality_tolerance(); + settings.tolerances.absolute_tolerance = pb_settings.absolute_tolerance(); + settings.tolerances.relative_tolerance = pb_settings.relative_tolerance(); + settings.tolerances.presolve_absolute_tolerance = pb_settings.presolve_absolute_tolerance(); + settings.log_to_console = pb_settings.log_to_console(); + settings.heuristics_only = pb_settings.heuristics_only(); + settings.num_cpu_threads = pb_settings.num_cpu_threads(); + settings.num_gpus = pb_settings.num_gpus(); + settings.presolver = static_cast(pb_settings.presolver()); + settings.mip_scaling = pb_settings.mip_scaling(); + settings.work_limit = pb_settings.work_limit(); + if (pb_settings.node_limit() >= 0) { + settings.node_limit = static_cast(pb_settings.node_limit()); + } + settings.reliability_branching = pb_settings.reliability_branching(); + settings.mip_batch_pdlp_strong_branching = pb_settings.mip_batch_pdlp_strong_branching(); + settings.mip_batch_pdlp_reliability_branching = pb_settings.mip_batch_pdlp_reliability_branching(); + settings.strong_branching_simplex_iteration_limit = pb_settings.strong_branching_simplex_iteration_limit(); + settings.max_cut_passes = pb_settings.max_cut_passes(); + settings.mir_cuts = pb_settings.mir_cuts(); + settings.mixed_integer_gomory_cuts = pb_settings.mixed_integer_gomory_cuts(); + settings.knapsack_cuts = pb_settings.knapsack_cuts(); + settings.clique_cuts = pb_settings.clique_cuts(); + settings.implied_bound_cuts = pb_settings.implied_bound_cuts(); + settings.strong_chvatal_gomory_cuts = pb_settings.strong_chvatal_gomory_cuts(); + settings.reduced_cost_strengthening = pb_settings.reduced_cost_strengthening(); + settings.cut_change_threshold = pb_settings.cut_change_threshold(); + settings.cut_min_orthogonality = pb_settings.cut_min_orthogonality(); + settings.determinism_mode = pb_settings.determinism_mode(); + settings.seed = pb_settings.seed(); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_solution.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_solution.inc new file mode 100644 index 0000000000..68aa029718 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_solution.inc @@ -0,0 +1,18 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + std::vector mip_solution(pb_solution.mip_solution().begin(), pb_solution.mip_solution().end()); + + auto _mip_termination_status = from_proto_mip_termination_status(pb_solution.mip_termination_status()); + auto _mip_objective = static_cast(pb_solution.mip_objective()); + auto _mip_gap = static_cast(pb_solution.mip_gap()); + auto _solution_bound = static_cast(pb_solution.solution_bound()); + auto _total_solve_time = static_cast(pb_solution.total_solve_time()); + auto _presolve_time = static_cast(pb_solution.presolve_time()); + auto _max_constraint_violation = static_cast(pb_solution.max_constraint_violation()); + auto _max_int_violation = static_cast(pb_solution.max_int_violation()); + auto _max_variable_bound_violation = static_cast(pb_solution.max_variable_bound_violation()); + auto _nodes = static_cast(pb_solution.nodes()); + auto _simplex_iterations = static_cast(pb_solution.simplex_iterations()); + return cpu_mip_solution_t(std::move(mip_solution), _mip_termination_status, _mip_objective, _mip_gap, _solution_bound, _total_solve_time, _presolve_time, _max_constraint_violation, _max_int_violation, _max_variable_bound_violation, _nodes, _simplex_iterations); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc new file mode 100644 index 0000000000..7f1de3bbd2 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc @@ -0,0 +1,36 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + settings.tolerances.absolute_gap_tolerance = pb_settings.absolute_gap_tolerance(); + settings.tolerances.relative_gap_tolerance = pb_settings.relative_gap_tolerance(); + settings.tolerances.primal_infeasible_tolerance = pb_settings.primal_infeasible_tolerance(); + settings.tolerances.dual_infeasible_tolerance = pb_settings.dual_infeasible_tolerance(); + settings.tolerances.absolute_dual_tolerance = pb_settings.absolute_dual_tolerance(); + settings.tolerances.relative_dual_tolerance = pb_settings.relative_dual_tolerance(); + settings.tolerances.absolute_primal_tolerance = pb_settings.absolute_primal_tolerance(); + settings.tolerances.relative_primal_tolerance = pb_settings.relative_primal_tolerance(); + settings.time_limit = pb_settings.time_limit(); + if (pb_settings.iteration_limit() >= 0) { + settings.iteration_limit = static_cast(pb_settings.iteration_limit()); + } + settings.log_to_console = pb_settings.log_to_console(); + settings.detect_infeasibility = pb_settings.detect_infeasibility(); + settings.strict_infeasibility = pb_settings.strict_infeasibility(); + settings.pdlp_solver_mode = from_proto_pdlp_solver_mode(pb_settings.pdlp_solver_mode()); + settings.method = from_proto_lp_method(pb_settings.method()); + settings.presolver = static_cast(pb_settings.presolver()); + settings.dual_postsolve = pb_settings.dual_postsolve(); + settings.crossover = pb_settings.crossover(); + settings.num_gpus = pb_settings.num_gpus(); + settings.per_constraint_residual = pb_settings.per_constraint_residual(); + settings.cudss_deterministic = pb_settings.cudss_deterministic(); + settings.folding = pb_settings.folding(); + settings.augmented = pb_settings.augmented(); + settings.dualize = pb_settings.dualize(); + settings.ordering = pb_settings.ordering(); + settings.barrier_dual_initial_point = pb_settings.barrier_dual_initial_point(); + settings.eliminate_dense_columns = pb_settings.eliminate_dense_columns(); + settings.save_best_primal_so_far = pb_settings.save_best_primal_so_far(); + settings.first_primal_feasible = pb_settings.first_primal_feasible(); + settings.pdlp_precision = static_cast(pb_settings.pdlp_precision()); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc new file mode 100644 index 0000000000..77c5a54e0d --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc @@ -0,0 +1,68 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ + cpu_problem.set_problem_name(pb_problem.problem_name()); + cpu_problem.set_objective_name(pb_problem.objective_name()); + cpu_problem.set_maximize(pb_problem.maximize()); + cpu_problem.set_objective_scaling_factor(pb_problem.objective_scaling_factor()); + cpu_problem.set_objective_offset(pb_problem.objective_offset()); + + if (pb_problem.a_values_size() > 0) { + std::vector A_values(pb_problem.a_values().begin(), pb_problem.a_values().end()); + std::vector A_indices(pb_problem.a_indices().begin(), pb_problem.a_indices().end()); + std::vector A_offsets(pb_problem.a_offsets().begin(), pb_problem.a_offsets().end()); + cpu_problem.set_csr_constraint_matrix(A_values.data(), static_cast(A_values.size()), A_indices.data(), static_cast(A_indices.size()), A_offsets.data(), static_cast(A_offsets.size())); + } + + if (pb_problem.q_values_size() > 0) { + std::vector Q_values(pb_problem.q_values().begin(), pb_problem.q_values().end()); + std::vector Q_indices(pb_problem.q_indices().begin(), pb_problem.q_indices().end()); + std::vector Q_offsets(pb_problem.q_offsets().begin(), pb_problem.q_offsets().end()); + cpu_problem.set_quadratic_objective_matrix(Q_values.data(), static_cast(Q_values.size()), Q_indices.data(), static_cast(Q_indices.size()), Q_offsets.data(), static_cast(Q_offsets.size())); + } + + if (pb_problem.variable_names_size() > 0) { + std::vector variable_names(pb_problem.variable_names().begin(), pb_problem.variable_names().end()); + cpu_problem.set_variable_names(variable_names); + } + if (pb_problem.row_names_size() > 0) { + std::vector row_names(pb_problem.row_names().begin(), pb_problem.row_names().end()); + cpu_problem.set_row_names(row_names); + } + { + std::vector c(pb_problem.c().begin(), pb_problem.c().end()); + cpu_problem.set_objective_coefficients(c.data(), static_cast(c.size())); + } + if (pb_problem.b_size() > 0) { + std::vector b(pb_problem.b().begin(), pb_problem.b().end()); + cpu_problem.set_constraint_bounds(b.data(), static_cast(b.size())); + } + { + std::vector variable_lower_bounds(pb_problem.variable_lower_bounds().begin(), pb_problem.variable_lower_bounds().end()); + cpu_problem.set_variable_lower_bounds(variable_lower_bounds.data(), static_cast(variable_lower_bounds.size())); + } + { + std::vector variable_upper_bounds(pb_problem.variable_upper_bounds().begin(), pb_problem.variable_upper_bounds().end()); + cpu_problem.set_variable_upper_bounds(variable_upper_bounds.data(), static_cast(variable_upper_bounds.size())); + } + if (pb_problem.constraint_lower_bounds_size() > 0) { + std::vector constraint_lower_bounds(pb_problem.constraint_lower_bounds().begin(), pb_problem.constraint_lower_bounds().end()); + cpu_problem.set_constraint_lower_bounds(constraint_lower_bounds.data(), static_cast(constraint_lower_bounds.size())); + } + if (pb_problem.constraint_upper_bounds_size() > 0) { + std::vector constraint_upper_bounds(pb_problem.constraint_upper_bounds().begin(), pb_problem.constraint_upper_bounds().end()); + cpu_problem.set_constraint_upper_bounds(constraint_upper_bounds.data(), static_cast(constraint_upper_bounds.size())); + } + if (!pb_problem.row_types().empty()) { + const std::string& row_types_str = pb_problem.row_types(); + cpu_problem.set_row_types(row_types_str.data(), static_cast(row_types_str.size())); + } + if (pb_problem.variable_types_size() > 0) { + std::vector variable_types; + variable_types.reserve(pb_problem.variable_types_size()); + for (const auto& v : pb_problem.variable_types()) { + variable_types.push_back(from_proto_variable_type(static_cast(v))); + } + cpu_problem.set_variable_types(variable_types.data(), static_cast(variable_types.size())); + } diff --git a/cpp/src/grpc/codegen/generated/generated_result_enums.proto.inc b/cpp/src/grpc/codegen/generated/generated_result_enums.proto.inc new file mode 100644 index 0000000000..bbf829e3b1 --- /dev/null +++ b/cpp/src/grpc/codegen/generated/generated_result_enums.proto.inc @@ -0,0 +1,27 @@ +// ============================================================================ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py +// ============================================================================ +// ResultFieldId enum entries + option allow_alias = true; + RESULT_PRIMAL_SOLUTION = 0; + RESULT_DUAL_SOLUTION = 1; + RESULT_REDUCED_COST = 2; + RESULT_WS_CURRENT_PRIMAL_SOLUTION = 3; + RESULT_WS_CURRENT_DUAL_SOLUTION = 4; + RESULT_WS_INITIAL_PRIMAL_AVERAGE = 5; + RESULT_WS_INITIAL_DUAL_AVERAGE = 6; + RESULT_WS_CURRENT_ATY = 7; + RESULT_WS_SUM_PRIMAL_SOLUTIONS = 8; + RESULT_WS_SUM_DUAL_SOLUTIONS = 9; + RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION = 10; + RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION = 11; + RESULT_MIP_SOLUTION = 12; + RESULT_WS_CURRENT_PRIMAL = 3; + RESULT_WS_CURRENT_DUAL = 4; + RESULT_WS_INITIAL_PRIMAL_AVG = 5; + RESULT_WS_INITIAL_DUAL_AVG = 6; + RESULT_WS_SUM_PRIMAL = 8; + RESULT_WS_SUM_DUAL = 9; + RESULT_WS_LAST_RESTART_GAP_PRIMAL = 10; + RESULT_WS_LAST_RESTART_GAP_DUAL = 11; diff --git a/cpp/src/grpc/cuopt_remote.proto b/cpp/src/grpc/cuopt_remote.proto index d58145a8e6..c14298248f 100644 --- a/cpp/src/grpc/cuopt_remote.proto +++ b/cpp/src/grpc/cuopt_remote.proto @@ -1,202 +1,21 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; package cuopt.remote; +// Data type definitions (enums, messages for problem/solution/settings) are +// auto-generated from field_registry.yaml. Using "import public" so that any +// file importing cuopt_remote.proto also sees the data types. +import public "cuopt_remote_data.proto"; + // Protocol version and metadata message RequestHeader { uint32 version = 1; // Protocol version (currently 1) ProblemCategory problem_category = 2; // LP or MIP } -enum ProblemCategory { - LP = 0; - MIP = 1; -} - -enum VariableType { - CONTINUOUS = 0; - INTEGER = 1; -} - -// Optimization problem representation (field names match cpu_optimization_problem_t) -message OptimizationProblem { - // Problem metadata - string problem_name = 1; - string objective_name = 2; - bool maximize = 3; - double objective_scaling_factor = 4; - double objective_offset = 5; - - // Variable and row names (optional) - repeated string variable_names = 7; - repeated string row_names = 8; - - // Constraint matrix A in CSR format - repeated double A_values = 9; - repeated int32 A_indices = 10; - repeated int32 A_offsets = 11; - - // Problem vectors - repeated double c = 12; // objective coefficients - repeated double b = 13; // constraint bounds (RHS) - repeated double variable_lower_bounds = 14; - repeated double variable_upper_bounds = 15; - - // Constraint bounds (alternative to b + row_types) - repeated double constraint_lower_bounds = 16; - repeated double constraint_upper_bounds = 17; - bytes row_types = 18; // char array: 'E' (=), 'L' (<=), 'G' (>=), 'N' (objective) - - // Variable types (enum-based: CONTINUOUS or INTEGER) - repeated VariableType variable_types = 19; - - // Initial solutions - repeated double initial_primal_solution = 20; - repeated double initial_dual_solution = 21; - - // Quadratic objective matrix Q in CSR format - repeated double Q_values = 22; - repeated int32 Q_indices = 23; - repeated int32 Q_offsets = 24; -} - -// PDLP solver mode enum (matches cuOpt pdlp_solver_mode_t) -enum PDLPSolverMode { - Stable1 = 0; - Stable2 = 1; - Methodical1 = 2; - Fast1 = 3; - Stable3 = 4; -} - -// Matches cuOpt method_t enum values -enum LPMethod { - Concurrent = 0; - PDLP = 1; - DualSimplex = 2; - Barrier = 3; -} - -// PDLP solver settings (field names match cuOpt Python/C++ API) -// Dense numbering: tolerances 1-8, limits 9-10, config 11-30 -message PDLPSolverSettings { - // Termination tolerances - double absolute_gap_tolerance = 1; - double relative_gap_tolerance = 2; - double primal_infeasible_tolerance = 3; - double dual_infeasible_tolerance = 4; - double absolute_dual_tolerance = 5; - double relative_dual_tolerance = 6; - double absolute_primal_tolerance = 7; - double relative_primal_tolerance = 8; - - // Limits - double time_limit = 9; - // Iteration limit. Sentinel: set to -1 to mean "unset/use server defaults". - // Note: proto3 numeric fields default to 0 when omitted, so clients should - // explicitly use -1 (or a positive value) to avoid accidentally requesting 0 iterations. - int64 iteration_limit = 10; - - // Solver configuration - bool log_to_console = 11; - bool detect_infeasibility = 12; - bool strict_infeasibility = 13; - PDLPSolverMode pdlp_solver_mode = 14; - LPMethod method = 15; - int32 presolver = 16; - bool dual_postsolve = 17; - bool crossover = 18; - int32 num_gpus = 19; - - bool per_constraint_residual = 20; - bool cudss_deterministic = 21; - int32 folding = 22; - int32 augmented = 23; - int32 dualize = 24; - int32 ordering = 25; - int32 barrier_dual_initial_point = 26; - bool eliminate_dense_columns = 27; - bool save_best_primal_so_far = 28; - bool first_primal_feasible = 29; - int32 pdlp_precision = 30; - - // Warm start data (if provided) - PDLPWarmStartData warm_start_data = 50; -} - -// PDLP warm start data for continuing from a previous solve. -// Array field numbers 1-9, scalar field numbers in 3000-range -// (shared with ChunkedResultHeader for consistent chunked transfer numbering). -message PDLPWarmStartData { - repeated double current_primal_solution = 1; - repeated double current_dual_solution = 2; - repeated double initial_primal_average = 3; - repeated double initial_dual_average = 4; - repeated double current_ATY = 5; - repeated double sum_primal_solutions = 6; - repeated double sum_dual_solutions = 7; - repeated double last_restart_duality_gap_primal_solution = 8; - repeated double last_restart_duality_gap_dual_solution = 9; - - double initial_primal_weight = 3000; - double initial_step_size = 3001; - int32 total_pdlp_iterations = 3002; - int32 total_pdhg_iterations = 3003; - double last_candidate_kkt_score = 3004; - double last_restart_kkt_score = 3005; - double sum_solution_weight = 3006; - int32 iterations_since_last_restart = 3007; -} - -// MIP solver settings (field names match cuOpt Python/C++ API) -// Dense numbering 1-28 -message MIPSolverSettings { - // Limits - double time_limit = 1; - - // Tolerances - double relative_mip_gap = 2; - double absolute_mip_gap = 3; - double integrality_tolerance = 4; - double absolute_tolerance = 5; - double relative_tolerance = 6; - double presolve_absolute_tolerance = 7; - - // Solver configuration - bool log_to_console = 8; - bool heuristics_only = 9; - int32 num_cpu_threads = 10; - int32 num_gpus = 11; - int32 presolver = 12; - int32 mip_scaling = 13; - - // Additional limits - double work_limit = 14; - int32 node_limit = 15; - - // Branching - int32 reliability_branching = 16; - int32 mip_batch_pdlp_strong_branching = 17; - - // Cut configuration - int32 max_cut_passes = 18; - int32 mir_cuts = 19; - int32 mixed_integer_gomory_cuts = 20; - int32 knapsack_cuts = 21; - int32 clique_cuts = 22; - int32 strong_chvatal_gomory_cuts = 23; - int32 reduced_cost_strengthening = 24; - double cut_change_threshold = 25; - double cut_min_orthogonality = 26; - - // Determinism and reproducibility - int32 determinism_mode = 27; - int32 seed = 28; -} - // LP solve request message SolveLPRequest { RequestHeader header = 1; @@ -212,118 +31,6 @@ message SolveMIPRequest { optional bool enable_incumbents = 4; } -// LP solution -// Array field numbers 1-4, scalar field numbers in 1000-range -// (shared with ChunkedResultHeader for consistent chunked transfer numbering). -message LPSolution { - // Solution vectors - repeated double primal_solution = 1; - repeated double dual_solution = 2; - repeated double reduced_cost = 3; - - // Warm start data for next solve - PDLPWarmStartData warm_start_data = 4; - - // Termination information - PDLPTerminationStatus lp_termination_status = 1000; - string error_message = 1001; - - // Solution statistics - double l2_primal_residual = 1002; - double l2_dual_residual = 1003; - double primal_objective = 1004; - double dual_objective = 1005; - double gap = 1006; - int32 nb_iterations = 1007; - double solve_time = 1008; - int32 solved_by = 1009; -} - -enum PDLPTerminationStatus { - PDLP_NO_TERMINATION = 0; - PDLP_NUMERICAL_ERROR = 1; - PDLP_OPTIMAL = 2; - PDLP_PRIMAL_INFEASIBLE = 3; - PDLP_DUAL_INFEASIBLE = 4; - PDLP_ITERATION_LIMIT = 5; - PDLP_TIME_LIMIT = 6; - PDLP_CONCURRENT_LIMIT = 7; - PDLP_PRIMAL_FEASIBLE = 8; -} - -// MIP solution -// Array field number 1, scalar field numbers in 2000-range -// (shared with ChunkedResultHeader for consistent chunked transfer numbering). -message MIPSolution { - repeated double mip_solution = 1; - - MIPTerminationStatus mip_termination_status = 2000; - string mip_error_message = 2001; - - double mip_objective = 2002; - double mip_gap = 2003; - double solution_bound = 2004; - double total_solve_time = 2005; - double presolve_time = 2006; - double max_constraint_violation = 2007; - double max_int_violation = 2008; - double max_variable_bound_violation = 2009; - int32 nodes = 2010; - int32 simplex_iterations = 2011; -} - -enum MIPTerminationStatus { - MIP_NO_TERMINATION = 0; - MIP_OPTIMAL = 1; - MIP_FEASIBLE_FOUND = 2; - MIP_INFEASIBLE = 3; - MIP_UNBOUNDED = 4; - MIP_TIME_LIMIT = 5; - MIP_WORK_LIMIT = 6; -} - -// Array field identifiers for chunked array transfers. -// Numbering matches codegen field_registry.yaml array_id values. -enum ArrayFieldId { - FIELD_VARIABLE_NAMES = 0; - FIELD_ROW_NAMES = 1; - FIELD_A_VALUES = 2; - FIELD_A_INDICES = 3; - FIELD_A_OFFSETS = 4; - FIELD_C = 5; - FIELD_B = 6; - FIELD_VARIABLE_LOWER_BOUNDS = 7; - FIELD_VARIABLE_UPPER_BOUNDS = 8; - FIELD_CONSTRAINT_LOWER_BOUNDS = 9; - FIELD_CONSTRAINT_UPPER_BOUNDS = 10; - FIELD_ROW_TYPES = 11; - FIELD_VARIABLE_TYPES = 12; - FIELD_INITIAL_PRIMAL_SOLUTION = 13; - FIELD_INITIAL_DUAL_SOLUTION = 14; - FIELD_Q_VALUES = 15; - FIELD_Q_INDICES = 16; - FIELD_Q_OFFSETS = 17; -} - -// Result array field identifiers for chunked result downloads. -// Numbering matches codegen field_registry.yaml array_id values. -enum ResultFieldId { - RESULT_PRIMAL_SOLUTION = 0; - RESULT_DUAL_SOLUTION = 1; - RESULT_REDUCED_COST = 2; - // Warm start arrays (LP only) - RESULT_WS_CURRENT_PRIMAL = 3; - RESULT_WS_CURRENT_DUAL = 4; - RESULT_WS_INITIAL_PRIMAL_AVG = 5; - RESULT_WS_INITIAL_DUAL_AVG = 6; - RESULT_WS_CURRENT_ATY = 7; - RESULT_WS_SUM_PRIMAL = 8; - RESULT_WS_SUM_DUAL = 9; - RESULT_WS_LAST_RESTART_GAP_PRIMAL = 10; - RESULT_WS_LAST_RESTART_GAP_DUAL = 11; - RESULT_MIP_SOLUTION = 12; -} - // Job status for async operations enum JobStatus { QUEUED = 0; // Job submitted, waiting in queue diff --git a/cpp/src/grpc/cuopt_remote_data.proto b/cpp/src/grpc/cuopt_remote_data.proto new file mode 100644 index 0000000000..09249ba8f5 --- /dev/null +++ b/cpp/src/grpc/cuopt_remote_data.proto @@ -0,0 +1,287 @@ +// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml +// DO NOT EDIT — regenerate with: python cpp/src/grpc/codegen/generate_conversions.py + +syntax = "proto3"; + +package cuopt.remote; + +enum PDLPTerminationStatus { + PDLP_NO_TERMINATION = 0; + PDLP_NUMERICAL_ERROR = 1; + PDLP_OPTIMAL = 2; + PDLP_PRIMAL_INFEASIBLE = 3; + PDLP_DUAL_INFEASIBLE = 4; + PDLP_ITERATION_LIMIT = 5; + PDLP_TIME_LIMIT = 6; + PDLP_CONCURRENT_LIMIT = 7; + PDLP_PRIMAL_FEASIBLE = 8; +} + +enum MIPTerminationStatus { + MIP_NO_TERMINATION = 0; + MIP_OPTIMAL = 1; + MIP_FEASIBLE_FOUND = 2; + MIP_INFEASIBLE = 3; + MIP_UNBOUNDED = 4; + MIP_TIME_LIMIT = 5; + MIP_WORK_LIMIT = 6; +} + +enum PDLPSolverMode { + Stable1 = 0; + Stable2 = 1; + Methodical1 = 2; + Fast1 = 3; + Stable3 = 4; +} + +enum LPMethod { + Concurrent = 0; + PDLP = 1; + DualSimplex = 2; + Barrier = 3; +} + +enum VariableType { + CONTINUOUS = 0; + INTEGER = 1; +} + +enum ProblemCategory { + LP = 0; + MIP = 1; +} + +enum ResultFieldId { + option allow_alias = true; + RESULT_PRIMAL_SOLUTION = 0; + RESULT_DUAL_SOLUTION = 1; + RESULT_REDUCED_COST = 2; + RESULT_WS_CURRENT_PRIMAL_SOLUTION = 3; + RESULT_WS_CURRENT_DUAL_SOLUTION = 4; + RESULT_WS_INITIAL_PRIMAL_AVERAGE = 5; + RESULT_WS_INITIAL_DUAL_AVERAGE = 6; + RESULT_WS_CURRENT_ATY = 7; + RESULT_WS_SUM_PRIMAL_SOLUTIONS = 8; + RESULT_WS_SUM_DUAL_SOLUTIONS = 9; + RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION = 10; + RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION = 11; + RESULT_MIP_SOLUTION = 12; + RESULT_WS_CURRENT_PRIMAL = 3; + RESULT_WS_CURRENT_DUAL = 4; + RESULT_WS_INITIAL_PRIMAL_AVG = 5; + RESULT_WS_INITIAL_DUAL_AVG = 6; + RESULT_WS_SUM_PRIMAL = 8; + RESULT_WS_SUM_DUAL = 9; + RESULT_WS_LAST_RESTART_GAP_PRIMAL = 10; + RESULT_WS_LAST_RESTART_GAP_DUAL = 11; +} + +enum ArrayFieldId { + FIELD_VARIABLE_NAMES = 0; + FIELD_ROW_NAMES = 1; + FIELD_A_VALUES = 2; + FIELD_A_INDICES = 3; + FIELD_A_OFFSETS = 4; + FIELD_C = 5; + FIELD_B = 6; + FIELD_VARIABLE_LOWER_BOUNDS = 7; + FIELD_VARIABLE_UPPER_BOUNDS = 8; + FIELD_CONSTRAINT_LOWER_BOUNDS = 9; + FIELD_CONSTRAINT_UPPER_BOUNDS = 10; + FIELD_ROW_TYPES = 11; + FIELD_VARIABLE_TYPES = 12; + FIELD_INITIAL_PRIMAL_SOLUTION = 13; + FIELD_INITIAL_DUAL_SOLUTION = 14; + FIELD_Q_VALUES = 15; + FIELD_Q_INDICES = 16; + FIELD_Q_OFFSETS = 17; +} + +message OptimizationProblem { + string problem_name = 1; + string objective_name = 2; + bool maximize = 3; + double objective_scaling_factor = 4; + double objective_offset = 5; + repeated string variable_names = 7; + repeated string row_names = 8; + repeated double A_values = 9; + repeated int32 A_indices = 10; + repeated int32 A_offsets = 11; + repeated double c = 12; + repeated double b = 13; + repeated double variable_lower_bounds = 14; + repeated double variable_upper_bounds = 15; + repeated double constraint_lower_bounds = 16; + repeated double constraint_upper_bounds = 17; + bytes row_types = 18; + repeated VariableType variable_types = 19; + repeated double initial_primal_solution = 20; + repeated double initial_dual_solution = 21; + repeated double Q_values = 22; + repeated int32 Q_indices = 23; + repeated int32 Q_offsets = 24; +} + +message PDLPSolverSettings { + double absolute_gap_tolerance = 1; + double relative_gap_tolerance = 2; + double primal_infeasible_tolerance = 3; + double dual_infeasible_tolerance = 4; + double absolute_dual_tolerance = 5; + double relative_dual_tolerance = 6; + double absolute_primal_tolerance = 7; + double relative_primal_tolerance = 8; + double time_limit = 9; + int64 iteration_limit = 10; + bool log_to_console = 11; + bool detect_infeasibility = 12; + bool strict_infeasibility = 13; + PDLPSolverMode pdlp_solver_mode = 14; + LPMethod method = 15; + int32 presolver = 16; + bool dual_postsolve = 17; + bool crossover = 18; + int32 num_gpus = 19; + bool per_constraint_residual = 20; + bool cudss_deterministic = 21; + int32 folding = 22; + int32 augmented = 23; + int32 dualize = 24; + int32 ordering = 25; + int32 barrier_dual_initial_point = 26; + bool eliminate_dense_columns = 27; + bool save_best_primal_so_far = 28; + bool first_primal_feasible = 29; + int32 pdlp_precision = 30; + PDLPWarmStartData warm_start_data = 50; +} + +message MIPSolverSettings { + double time_limit = 1; + double relative_mip_gap = 2; + double absolute_mip_gap = 3; + double integrality_tolerance = 4; + double absolute_tolerance = 5; + double relative_tolerance = 6; + double presolve_absolute_tolerance = 7; + bool log_to_console = 8; + bool heuristics_only = 9; + int32 num_cpu_threads = 10; + int32 num_gpus = 11; + int32 presolver = 12; + int32 mip_scaling = 13; + double work_limit = 14; + int32 node_limit = 15; + int32 reliability_branching = 16; + int32 mip_batch_pdlp_strong_branching = 17; + int32 max_cut_passes = 18; + int32 mir_cuts = 19; + int32 mixed_integer_gomory_cuts = 20; + int32 knapsack_cuts = 21; + int32 clique_cuts = 22; + int32 strong_chvatal_gomory_cuts = 23; + int32 reduced_cost_strengthening = 24; + double cut_change_threshold = 25; + double cut_min_orthogonality = 26; + int32 determinism_mode = 27; + int32 seed = 28; + int32 mip_batch_pdlp_reliability_branching = 29; + int32 strong_branching_simplex_iteration_limit = 30; + int32 implied_bound_cuts = 31; +} + +message PDLPWarmStartData { + repeated double current_primal_solution = 1; + repeated double current_dual_solution = 2; + repeated double initial_primal_average = 3; + repeated double initial_dual_average = 4; + repeated double current_ATY = 5; + repeated double sum_primal_solutions = 6; + repeated double sum_dual_solutions = 7; + repeated double last_restart_duality_gap_primal_solution = 8; + repeated double last_restart_duality_gap_dual_solution = 9; + double initial_primal_weight = 3000; + double initial_step_size = 3001; + int32 total_pdlp_iterations = 3002; + int32 total_pdhg_iterations = 3003; + double last_candidate_kkt_score = 3004; + double last_restart_kkt_score = 3005; + double sum_solution_weight = 3006; + int32 iterations_since_last_restart = 3007; +} + +message LPSolution { + repeated double primal_solution = 1; + repeated double dual_solution = 2; + repeated double reduced_cost = 3; + PDLPWarmStartData warm_start_data = 4; + PDLPTerminationStatus lp_termination_status = 1000; + string error_message = 1001; + double l2_primal_residual = 1002; + double l2_dual_residual = 1003; + double primal_objective = 1004; + double dual_objective = 1005; + double gap = 1006; + int32 nb_iterations = 1007; + double solve_time = 1008; + int32 solved_by = 1009; +} + +message MIPSolution { + repeated double mip_solution = 1; + MIPTerminationStatus mip_termination_status = 2000; + string mip_error_message = 2001; + double mip_objective = 2002; + double mip_gap = 2003; + double solution_bound = 2004; + double total_solve_time = 2005; + double presolve_time = 2006; + double max_constraint_violation = 2007; + double max_int_violation = 2008; + double max_variable_bound_violation = 2009; + int32 nodes = 2010; + int32 simplex_iterations = 2011; +} + +message ResultArrayDescriptor { + ResultFieldId field_id = 1; + int64 total_elements = 2; + int64 element_size_bytes = 3; +} + +message ChunkedResultHeader { + ProblemCategory problem_category = 1; + repeated ResultArrayDescriptor arrays = 50; + PDLPTerminationStatus lp_termination_status = 1000; + string error_message = 1001; + double l2_primal_residual = 1002; + double l2_dual_residual = 1003; + double primal_objective = 1004; + double dual_objective = 1005; + double gap = 1006; + int32 nb_iterations = 1007; + double solve_time = 1008; + int32 solved_by = 1009; + MIPTerminationStatus mip_termination_status = 2000; + string mip_error_message = 2001; + double mip_objective = 2002; + double mip_gap = 2003; + double solution_bound = 2004; + double total_solve_time = 2005; + double presolve_time = 2006; + double max_constraint_violation = 2007; + double max_int_violation = 2008; + double max_variable_bound_violation = 2009; + int32 nodes = 2010; + int32 simplex_iterations = 2011; + double ws_initial_primal_weight = 3000; + double ws_initial_step_size = 3001; + int32 ws_total_pdlp_iterations = 3002; + int32 ws_total_pdhg_iterations = 3003; + double ws_last_candidate_kkt_score = 3004; + double ws_last_restart_kkt_score = 3005; + double ws_sum_solution_weight = 3006; + int32 ws_iterations_since_last_restart = 3007; +} diff --git a/cpp/src/grpc/cuopt_remote_service.proto b/cpp/src/grpc/cuopt_remote_service.proto index 16fb1a8d80..0212ff3857 100644 --- a/cpp/src/grpc/cuopt_remote_service.proto +++ b/cpp/src/grpc/cuopt_remote_service.proto @@ -120,6 +120,7 @@ message ChunkedProblemHeader { double objective_offset = 4; string problem_name = 5; string objective_name = 6; + ProblemCategory problem_category = 9; // String arrays (included here since they are rarely the size bottleneck) repeated string variable_names = 7; @@ -183,59 +184,8 @@ message GetResultRequest { string job_id = 1; } -// Metadata about a single result array available for chunked download -message ResultArrayDescriptor { - ResultFieldId field_id = 1; - int64 total_elements = 2; - int64 element_size_bytes = 3; // 8 for double, 4 for int32, etc. -} - -// Header for chunked result download - carries all scalar/enum/string fields -// from LPSolution or MIPSolution. Array data is sent via GetResultChunk. -// Field numbers use the same 1000/2000/3000 ranges as LPSolution, MIPSolution, -// and PDLPWarmStartData for consistency with the codegen numbering scheme. -message ChunkedResultHeader { - ProblemCategory problem_category = 1; - - // Array metadata so client knows what to fetch - repeated ResultArrayDescriptor arrays = 50; - - // LP result scalars (1000-range, same as LPSolution) - PDLPTerminationStatus lp_termination_status = 1000; - string error_message = 1001; - double l2_primal_residual = 1002; - double l2_dual_residual = 1003; - double primal_objective = 1004; - double dual_objective = 1005; - double gap = 1006; - int32 nb_iterations = 1007; - double solve_time = 1008; - int32 solved_by = 1009; - - // MIP result scalars (2000-range, same as MIPSolution) - MIPTerminationStatus mip_termination_status = 2000; - string mip_error_message = 2001; - double mip_objective = 2002; - double mip_gap = 2003; - double solution_bound = 2004; - double total_solve_time = 2005; - double presolve_time = 2006; - double max_constraint_violation = 2007; - double max_int_violation = 2008; - double max_variable_bound_violation = 2009; - int32 nodes = 2010; - int32 simplex_iterations = 2011; - - // LP warm start scalars (3000-range, same as PDLPWarmStartData) - double ws_initial_primal_weight = 3000; - double ws_initial_step_size = 3001; - int32 ws_total_pdlp_iterations = 3002; - int32 ws_total_pdhg_iterations = 3003; - double ws_last_candidate_kkt_score = 3004; - double ws_last_restart_kkt_score = 3005; - double ws_sum_solution_weight = 3006; - int32 ws_iterations_since_last_restart = 3007; -} +// ResultArrayDescriptor and ChunkedResultHeader are defined in +// cuopt_remote_data.proto (auto-generated from field_registry.yaml). message StartChunkedDownloadRequest { string job_id = 1; diff --git a/cpp/src/grpc/grpc_problem_mapper.cpp b/cpp/src/grpc/grpc_problem_mapper.cpp index bc5342defe..2cdf7890d0 100644 --- a/cpp/src/grpc/grpc_problem_mapper.cpp +++ b/cpp/src/grpc/grpc_problem_mapper.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include "grpc_settings_mapper.hpp" @@ -22,234 +21,81 @@ namespace cuopt::linear_programming { -template -void map_problem_to_proto(const cpu_optimization_problem_t& cpu_problem, - cuopt::remote::OptimizationProblem* pb_problem) -{ - // Basic problem metadata - pb_problem->set_problem_name(cpu_problem.get_problem_name()); - pb_problem->set_objective_name(cpu_problem.get_objective_name()); - pb_problem->set_maximize(cpu_problem.get_sense()); - pb_problem->set_objective_scaling_factor(cpu_problem.get_objective_scaling_factor()); - pb_problem->set_objective_offset(cpu_problem.get_objective_offset()); - - // Get constraint matrix data from host memory - auto values = cpu_problem.get_constraint_matrix_values_host(); - auto indices = cpu_problem.get_constraint_matrix_indices_host(); - auto offsets = cpu_problem.get_constraint_matrix_offsets_host(); - - // Constraint matrix A in CSR format - for (const auto& val : values) { - pb_problem->add_a_values(static_cast(val)); - } - for (const auto& idx : indices) { - pb_problem->add_a_indices(static_cast(idx)); - } - for (const auto& off : offsets) { - pb_problem->add_a_offsets(static_cast(off)); - } - - // Objective coefficients - auto obj_coeffs = cpu_problem.get_objective_coefficients_host(); - for (const auto& c : obj_coeffs) { - pb_problem->add_c(static_cast(c)); - } +namespace { +#include "generated_enum_converters_problem.inc" - // Variable bounds - auto var_lb = cpu_problem.get_variable_lower_bounds_host(); - auto var_ub = cpu_problem.get_variable_upper_bounds_host(); - for (const auto& lb : var_lb) { - pb_problem->add_variable_lower_bounds(static_cast(lb)); - } - for (const auto& ub : var_ub) { - pb_problem->add_variable_upper_bounds(static_cast(ub)); - } +template +void chunk_typed_array(std::vector& out, + cuopt::remote::ArrayFieldId field_id, + const std::vector& data, + const std::string& upload_id, + int64_t chunk_data_budget) +{ + if (data.empty()) return; - // Constraint bounds - auto con_lb = cpu_problem.get_constraint_lower_bounds_host(); - auto con_ub = cpu_problem.get_constraint_upper_bounds_host(); + const int64_t elem_size = static_cast(sizeof(T)); + const int64_t total_elements = static_cast(data.size()); - if (!con_lb.empty() && !con_ub.empty()) { - for (const auto& lb : con_lb) { - pb_problem->add_constraint_lower_bounds(static_cast(lb)); - } - for (const auto& ub : con_ub) { - pb_problem->add_constraint_upper_bounds(static_cast(ub)); - } - } + int64_t elems_per_chunk = chunk_data_budget / elem_size; + if (elems_per_chunk <= 0) elems_per_chunk = 1; - // Row types (if available) - auto row_types = cpu_problem.get_row_types_host(); - if (!row_types.empty()) { - pb_problem->set_row_types(std::string(row_types.begin(), row_types.end())); - } + const auto* raw = reinterpret_cast(data.data()); - // Constraint bounds (RHS) - if available - auto b = cpu_problem.get_constraint_bounds_host(); - if (!b.empty()) { - for (const auto& rhs : b) { - pb_problem->add_b(static_cast(rhs)); - } - } + for (int64_t offset = 0; offset < total_elements; offset += elems_per_chunk) { + int64_t count = std::min(elems_per_chunk, total_elements - offset); + int64_t byte_offset = offset * elem_size; + int64_t byte_count = count * elem_size; - // Variable names - const auto& var_names = cpu_problem.get_variable_names(); - for (const auto& name : var_names) { - pb_problem->add_variable_names(name); + cuopt::remote::SendArrayChunkRequest req; + req.set_upload_id(upload_id); + auto* ac = req.mutable_chunk(); + ac->set_field_id(field_id); + ac->set_element_offset(offset); + ac->set_total_elements(total_elements); + ac->set_data(raw + byte_offset, byte_count); + out.push_back(std::move(req)); } +} - // Row names - const auto& row_names = cpu_problem.get_row_names(); - for (const auto& name : row_names) { - pb_problem->add_row_names(name); - } +void chunk_byte_blob(std::vector& out, + cuopt::remote::ArrayFieldId field_id, + const std::vector& data, + const std::string& upload_id, + int64_t chunk_data_budget) +{ + chunk_typed_array(out, field_id, data, upload_id, chunk_data_budget); +} - // Variable types (for MIP problems) - auto var_types = cpu_problem.get_variable_types_host(); - if (!var_types.empty()) { - for (const auto& vt : var_types) { - switch (vt) { - case var_t::CONTINUOUS: pb_problem->add_variable_types(cuopt::remote::CONTINUOUS); break; - case var_t::INTEGER: pb_problem->add_variable_types(cuopt::remote::INTEGER); break; - default: - throw std::runtime_error("map_problem_to_proto: unknown var_t value " + - std::to_string(static_cast(vt))); - } - } - } +std::vector names_to_blob(const std::vector& names) +{ + if (names.empty()) return {}; + size_t total = 0; + for (const auto& n : names) + total += n.size() + 1; + std::vector blob(total); + size_t pos = 0; + for (const auto& n : names) { + std::memcpy(blob.data() + pos, n.data(), n.size()); + pos += n.size(); + blob[pos++] = '\0'; + } + return blob; +} - // Quadratic objective matrix Q (for QPS problems) - if (cpu_problem.has_quadratic_objective()) { - const auto& q_values = cpu_problem.get_quadratic_objective_values(); - const auto& q_indices = cpu_problem.get_quadratic_objective_indices(); - const auto& q_offsets = cpu_problem.get_quadratic_objective_offsets(); +} // namespace - for (const auto& val : q_values) { - pb_problem->add_q_values(static_cast(val)); - } - for (const auto& idx : q_indices) { - pb_problem->add_q_indices(static_cast(idx)); - } - for (const auto& off : q_offsets) { - pb_problem->add_q_offsets(static_cast(off)); - } - } +template +void map_problem_to_proto(const cpu_optimization_problem_t& cpu_problem, + cuopt::remote::OptimizationProblem* pb_problem) +{ +#include "generated_problem_to_proto.inc" } template void map_proto_to_problem(const cuopt::remote::OptimizationProblem& pb_problem, cpu_optimization_problem_t& cpu_problem) { - // Basic problem metadata - cpu_problem.set_problem_name(pb_problem.problem_name()); - cpu_problem.set_objective_name(pb_problem.objective_name()); - cpu_problem.set_maximize(pb_problem.maximize()); - cpu_problem.set_objective_scaling_factor(pb_problem.objective_scaling_factor()); - cpu_problem.set_objective_offset(pb_problem.objective_offset()); - - // Constraint matrix A in CSR format - std::vector values(pb_problem.a_values().begin(), pb_problem.a_values().end()); - std::vector indices(pb_problem.a_indices().begin(), pb_problem.a_indices().end()); - std::vector offsets(pb_problem.a_offsets().begin(), pb_problem.a_offsets().end()); - - cpu_problem.set_csr_constraint_matrix(values.data(), - static_cast(values.size()), - indices.data(), - static_cast(indices.size()), - offsets.data(), - static_cast(offsets.size())); - - // Objective coefficients - std::vector obj(pb_problem.c().begin(), pb_problem.c().end()); - cpu_problem.set_objective_coefficients(obj.data(), static_cast(obj.size())); - - // Variable bounds - std::vector var_lb(pb_problem.variable_lower_bounds().begin(), - pb_problem.variable_lower_bounds().end()); - std::vector var_ub(pb_problem.variable_upper_bounds().begin(), - pb_problem.variable_upper_bounds().end()); - cpu_problem.set_variable_lower_bounds(var_lb.data(), static_cast(var_lb.size())); - cpu_problem.set_variable_upper_bounds(var_ub.data(), static_cast(var_ub.size())); - - // Constraint bounds (prefer lower/upper bounds if available) - if (pb_problem.constraint_lower_bounds_size() > 0 && - pb_problem.constraint_upper_bounds_size() > 0 && - pb_problem.constraint_lower_bounds_size() == pb_problem.constraint_upper_bounds_size()) { - std::vector con_lb(pb_problem.constraint_lower_bounds().begin(), - pb_problem.constraint_lower_bounds().end()); - std::vector con_ub(pb_problem.constraint_upper_bounds().begin(), - pb_problem.constraint_upper_bounds().end()); - cpu_problem.set_constraint_lower_bounds(con_lb.data(), static_cast(con_lb.size())); - cpu_problem.set_constraint_upper_bounds(con_ub.data(), static_cast(con_ub.size())); - } else if (pb_problem.b_size() > 0) { - // Use b (RHS) + row_types format - std::vector b(pb_problem.b().begin(), pb_problem.b().end()); - cpu_problem.set_constraint_bounds(b.data(), static_cast(b.size())); - - if (!pb_problem.row_types().empty()) { - const std::string& row_types_str = pb_problem.row_types(); - cpu_problem.set_row_types(row_types_str.data(), static_cast(row_types_str.size())); - } - } - - // Variable names - if (pb_problem.variable_names_size() > 0) { - std::vector var_names(pb_problem.variable_names().begin(), - pb_problem.variable_names().end()); - cpu_problem.set_variable_names(var_names); - } - - // Row names - if (pb_problem.row_names_size() > 0) { - std::vector row_names(pb_problem.row_names().begin(), - pb_problem.row_names().end()); - cpu_problem.set_row_names(row_names); - } - - // Variable types - if (pb_problem.variable_types_size() > 0) { - std::vector var_types; - var_types.reserve(pb_problem.variable_types_size()); - for (int i = 0; i < pb_problem.variable_types_size(); ++i) { - switch (pb_problem.variable_types(i)) { - case cuopt::remote::CONTINUOUS: var_types.push_back(var_t::CONTINUOUS); break; - case cuopt::remote::INTEGER: var_types.push_back(var_t::INTEGER); break; - default: - throw std::runtime_error("Unknown VariableType enum value " + - std::to_string(pb_problem.variable_types(i))); - } - } - cpu_problem.set_variable_types(var_types.data(), static_cast(var_types.size())); - } - - // Quadratic objective matrix Q (for QPS problems) - if (pb_problem.q_values_size() > 0) { - std::vector q_values(pb_problem.q_values().begin(), pb_problem.q_values().end()); - std::vector q_indices(pb_problem.q_indices().begin(), pb_problem.q_indices().end()); - std::vector q_offsets(pb_problem.q_offsets().begin(), pb_problem.q_offsets().end()); - - cpu_problem.set_quadratic_objective_matrix(q_values.data(), - static_cast(q_values.size()), - q_indices.data(), - static_cast(q_indices.size()), - q_offsets.data(), - static_cast(q_offsets.size())); - } - - // Infer problem category from variable types - if (pb_problem.variable_types_size() > 0) { - bool has_integers = false; - for (int i = 0; i < pb_problem.variable_types_size(); ++i) { - if (pb_problem.variable_types(i) == cuopt::remote::INTEGER) { - has_integers = true; - break; - } - } - cpu_problem.set_problem_category(has_integers ? problem_category_t::MIP - : problem_category_t::LP); - } else { - cpu_problem.set_problem_category(problem_category_t::LP); - } +#include "generated_proto_to_problem.inc" } // ============================================================================ @@ -259,51 +105,7 @@ void map_proto_to_problem(const cuopt::remote::OptimizationProblem& pb_problem, template size_t estimate_problem_proto_size(const cpu_optimization_problem_t& cpu_problem) { - size_t est = 0; - - // Constraint matrix CSR arrays - auto values = cpu_problem.get_constraint_matrix_values_host(); - auto indices = cpu_problem.get_constraint_matrix_indices_host(); - auto offsets = cpu_problem.get_constraint_matrix_offsets_host(); - est += values.size() * sizeof(double); // packed repeated double - est += indices.size() * 5; // varint int32 (worst case 5 bytes each) - est += offsets.size() * 5; - - // Objective coefficients - est += cpu_problem.get_objective_coefficients_host().size() * sizeof(double); - - // Variable bounds - est += cpu_problem.get_variable_lower_bounds_host().size() * sizeof(double); - est += cpu_problem.get_variable_upper_bounds_host().size() * sizeof(double); - - // Constraint bounds - est += cpu_problem.get_constraint_lower_bounds_host().size() * sizeof(double); - est += cpu_problem.get_constraint_upper_bounds_host().size() * sizeof(double); - est += cpu_problem.get_constraint_bounds_host().size() * sizeof(double); - - // Row types and variable types - est += cpu_problem.get_row_types_host().size(); - est += cpu_problem.get_variable_types_host().size(); - - // Quadratic objective - if (cpu_problem.has_quadratic_objective()) { - est += cpu_problem.get_quadratic_objective_values().size() * sizeof(double); - est += cpu_problem.get_quadratic_objective_indices().size() * 5; - est += cpu_problem.get_quadratic_objective_offsets().size() * 5; - } - - // String arrays (rough estimate) - for (const auto& name : cpu_problem.get_variable_names()) { - est += name.size() + 2; // string + tag + length varint - } - for (const auto& name : cpu_problem.get_row_names()) { - est += name.size() + 2; - } - - // Protobuf overhead for tags, submessage lengths, etc. - est += 512; - - return est; +#include "generated_estimate_problem_size.inc" } // ============================================================================ @@ -315,22 +117,7 @@ void populate_chunked_header_lp(const cpu_optimization_problem_t& cpu_ const pdlp_solver_settings_t& settings, cuopt::remote::ChunkedProblemHeader* header) { - // Request header - auto* rh = header->mutable_header(); - rh->set_version(1); - rh->set_problem_category(cuopt::remote::LP); - - header->set_maximize(cpu_problem.get_sense()); - header->set_objective_scaling_factor(cpu_problem.get_objective_scaling_factor()); - header->set_objective_offset(cpu_problem.get_objective_offset()); - header->set_problem_name(cpu_problem.get_problem_name()); - header->set_objective_name(cpu_problem.get_objective_name()); - - // Variable/row names are sent as chunked arrays, not in the header, - // to avoid the header exceeding gRPC max message size for large problems. - - // LP settings - map_pdlp_settings_to_proto(settings, header->mutable_lp_settings()); +#include "generated_populate_chunked_header_lp.inc" } template @@ -339,22 +126,7 @@ void populate_chunked_header_mip(const cpu_optimization_problem_t& cpu bool enable_incumbents, cuopt::remote::ChunkedProblemHeader* header) { - // Request header - auto* rh = header->mutable_header(); - rh->set_version(1); - rh->set_problem_category(cuopt::remote::MIP); - - header->set_maximize(cpu_problem.get_sense()); - header->set_objective_scaling_factor(cpu_problem.get_objective_scaling_factor()); - header->set_objective_offset(cpu_problem.get_objective_offset()); - header->set_problem_name(cpu_problem.get_problem_name()); - header->set_objective_name(cpu_problem.get_objective_name()); - - // Variable/row names are sent as chunked arrays, not in the header. - - // MIP settings - map_mip_settings_to_proto(settings, header->mutable_mip_settings()); - header->set_enable_incumbents(enable_incumbents); +#include "generated_populate_chunked_header_mip.inc" } // ============================================================================ @@ -365,24 +137,7 @@ template void map_chunked_header_to_problem(const cuopt::remote::ChunkedProblemHeader& header, cpu_optimization_problem_t& cpu_problem) { - cpu_problem.set_problem_name(header.problem_name()); - cpu_problem.set_objective_name(header.objective_name()); - cpu_problem.set_maximize(header.maximize()); - cpu_problem.set_objective_scaling_factor(header.objective_scaling_factor()); - cpu_problem.set_objective_offset(header.objective_offset()); - - // String arrays - if (header.variable_names_size() > 0) { - std::vector var_names(header.variable_names().begin(), - header.variable_names().end()); - cpu_problem.set_variable_names(var_names); - } - if (header.row_names_size() > 0) { - std::vector row_names(header.row_names().begin(), header.row_names().end()); - cpu_problem.set_row_names(row_names); - } - - // Problem category inferred later when variable_types array is set +#include "generated_chunked_header_to_problem.inc" } // ============================================================================ @@ -394,297 +149,20 @@ void map_chunked_arrays_to_problem(const cuopt::remote::ChunkedProblemHeader& he const std::map>& arrays, cpu_optimization_problem_t& cpu_problem) { - map_chunked_header_to_problem(header, cpu_problem); - - auto get_doubles = [&](int32_t field_id) -> std::vector { - auto it = arrays.find(field_id); - if (it == arrays.end() || it->second.empty()) return {}; - if (it->second.size() % sizeof(double) != 0) return {}; - size_t n = it->second.size() / sizeof(double); - if constexpr (std::is_same_v) { - std::vector v(n); - std::memcpy(v.data(), it->second.data(), n * sizeof(double)); - return v; - } else { - std::vector tmp(n); - std::memcpy(tmp.data(), it->second.data(), n * sizeof(double)); - return std::vector(tmp.begin(), tmp.end()); - } - }; - - auto get_ints = [&](int32_t field_id) -> std::vector { - auto it = arrays.find(field_id); - if (it == arrays.end() || it->second.empty()) return {}; - if (it->second.size() % sizeof(int32_t) != 0) return {}; - size_t n = it->second.size() / sizeof(int32_t); - if constexpr (std::is_same_v) { - std::vector v(n); - std::memcpy(v.data(), it->second.data(), n * sizeof(int32_t)); - return v; - } else { - std::vector tmp(n); - std::memcpy(tmp.data(), it->second.data(), n * sizeof(int32_t)); - return std::vector(tmp.begin(), tmp.end()); - } - }; - - auto get_bytes = [&](int32_t field_id) -> std::string { - auto it = arrays.find(field_id); - if (it == arrays.end() || it->second.empty()) return {}; - return std::string(reinterpret_cast(it->second.data()), it->second.size()); - }; - - auto get_string_list = [&](int32_t field_id) -> std::vector { - auto it = arrays.find(field_id); - if (it == arrays.end() || it->second.empty()) return {}; - std::vector names; - const char* s = reinterpret_cast(it->second.data()); - const char* s_end = s + it->second.size(); - while (s < s_end) { - const char* nul = static_cast(std::memchr(s, '\0', s_end - s)); - if (!nul) nul = s_end; - names.emplace_back(s, nul); - if (nul == s_end) break; - s = nul + 1; - } - return names; - }; - - // CSR constraint matrix - auto a_values = get_doubles(cuopt::remote::FIELD_A_VALUES); - auto a_indices = get_ints(cuopt::remote::FIELD_A_INDICES); - auto a_offsets = get_ints(cuopt::remote::FIELD_A_OFFSETS); - if (!a_values.empty() && !a_indices.empty() && !a_offsets.empty()) { - cpu_problem.set_csr_constraint_matrix(a_values.data(), - static_cast(a_values.size()), - a_indices.data(), - static_cast(a_indices.size()), - a_offsets.data(), - static_cast(a_offsets.size())); - } - - // Objective coefficients - auto c_vec = get_doubles(cuopt::remote::FIELD_C); - if (!c_vec.empty()) { - cpu_problem.set_objective_coefficients(c_vec.data(), static_cast(c_vec.size())); - } - - // Variable bounds - auto var_lb = get_doubles(cuopt::remote::FIELD_VARIABLE_LOWER_BOUNDS); - auto var_ub = get_doubles(cuopt::remote::FIELD_VARIABLE_UPPER_BOUNDS); - if (!var_lb.empty()) { - cpu_problem.set_variable_lower_bounds(var_lb.data(), static_cast(var_lb.size())); - } - if (!var_ub.empty()) { - cpu_problem.set_variable_upper_bounds(var_ub.data(), static_cast(var_ub.size())); - } - - // Constraint bounds - auto con_lb = get_doubles(cuopt::remote::FIELD_CONSTRAINT_LOWER_BOUNDS); - auto con_ub = get_doubles(cuopt::remote::FIELD_CONSTRAINT_UPPER_BOUNDS); - if (!con_lb.empty()) { - cpu_problem.set_constraint_lower_bounds(con_lb.data(), static_cast(con_lb.size())); - } - if (!con_ub.empty()) { - cpu_problem.set_constraint_upper_bounds(con_ub.data(), static_cast(con_ub.size())); - } - - auto b_vec = get_doubles(cuopt::remote::FIELD_B); - if (!b_vec.empty()) { - cpu_problem.set_constraint_bounds(b_vec.data(), static_cast(b_vec.size())); - } - - // Row types - auto row_types_str = get_bytes(cuopt::remote::FIELD_ROW_TYPES); - if (!row_types_str.empty()) { - cpu_problem.set_row_types(row_types_str.data(), static_cast(row_types_str.size())); - } - - // Variable types + problem category - auto var_types_ints = get_ints(cuopt::remote::FIELD_VARIABLE_TYPES); - if (!var_types_ints.empty()) { - std::vector vtypes; - vtypes.reserve(var_types_ints.size()); - bool has_ints = false; - for (const auto& v : var_types_ints) { - switch (static_cast(v)) { - case cuopt::remote::CONTINUOUS: vtypes.push_back(var_t::CONTINUOUS); break; - case cuopt::remote::INTEGER: - vtypes.push_back(var_t::INTEGER); - has_ints = true; - break; - default: - throw std::runtime_error("Unknown VariableType enum value " + std::to_string(v) + - " in chunked variable_types"); - } - } - cpu_problem.set_variable_types(vtypes.data(), static_cast(vtypes.size())); - cpu_problem.set_problem_category(has_ints ? problem_category_t::MIP : problem_category_t::LP); - } else { - cpu_problem.set_problem_category(problem_category_t::LP); - } - - // Quadratic objective - auto q_values = get_doubles(cuopt::remote::FIELD_Q_VALUES); - auto q_indices = get_ints(cuopt::remote::FIELD_Q_INDICES); - auto q_offsets = get_ints(cuopt::remote::FIELD_Q_OFFSETS); - if (!q_values.empty() && !q_indices.empty() && !q_offsets.empty()) { - cpu_problem.set_quadratic_objective_matrix(q_values.data(), - static_cast(q_values.size()), - q_indices.data(), - static_cast(q_indices.size()), - q_offsets.data(), - static_cast(q_offsets.size())); - } - - // String arrays (may also be in header; these override if present as chunked arrays) - auto var_names = get_string_list(cuopt::remote::FIELD_VARIABLE_NAMES); - if (!var_names.empty()) { cpu_problem.set_variable_names(var_names); } - auto row_names = get_string_list(cuopt::remote::FIELD_ROW_NAMES); - if (!row_names.empty()) { cpu_problem.set_row_names(row_names); } +#include "generated_chunked_arrays_to_problem.inc" } // ============================================================================= // Chunked array request building (client-side) // ============================================================================= -namespace { - -template -void chunk_typed_array(std::vector& out, - cuopt::remote::ArrayFieldId field_id, - const std::vector& data, - const std::string& upload_id, - int64_t chunk_data_budget) -{ - if (data.empty()) return; - - const int64_t elem_size = static_cast(sizeof(T)); - const int64_t total_elements = static_cast(data.size()); - - int64_t elems_per_chunk = chunk_data_budget / elem_size; - if (elems_per_chunk <= 0) elems_per_chunk = 1; - - const auto* raw = reinterpret_cast(data.data()); - - for (int64_t offset = 0; offset < total_elements; offset += elems_per_chunk) { - int64_t count = std::min(elems_per_chunk, total_elements - offset); - int64_t byte_offset = offset * elem_size; - int64_t byte_count = count * elem_size; - - cuopt::remote::SendArrayChunkRequest req; - req.set_upload_id(upload_id); - auto* ac = req.mutable_chunk(); - ac->set_field_id(field_id); - ac->set_element_offset(offset); - ac->set_total_elements(total_elements); - ac->set_data(raw + byte_offset, byte_count); - out.push_back(std::move(req)); - } -} - -void chunk_byte_blob(std::vector& out, - cuopt::remote::ArrayFieldId field_id, - const std::vector& data, - const std::string& upload_id, - int64_t chunk_data_budget) -{ - chunk_typed_array(out, field_id, data, upload_id, chunk_data_budget); -} - -} // namespace - template std::vector build_array_chunk_requests( const cpu_optimization_problem_t& problem, const std::string& upload_id, int64_t chunk_size_bytes) { - std::vector requests; - - auto values = problem.get_constraint_matrix_values_host(); - auto indices = problem.get_constraint_matrix_indices_host(); - auto offsets = problem.get_constraint_matrix_offsets_host(); - auto obj = problem.get_objective_coefficients_host(); - auto var_lb = problem.get_variable_lower_bounds_host(); - auto var_ub = problem.get_variable_upper_bounds_host(); - auto con_lb = problem.get_constraint_lower_bounds_host(); - auto con_ub = problem.get_constraint_upper_bounds_host(); - auto b = problem.get_constraint_bounds_host(); - - chunk_typed_array(requests, cuopt::remote::FIELD_A_VALUES, values, upload_id, chunk_size_bytes); - chunk_typed_array(requests, cuopt::remote::FIELD_A_INDICES, indices, upload_id, chunk_size_bytes); - chunk_typed_array(requests, cuopt::remote::FIELD_A_OFFSETS, offsets, upload_id, chunk_size_bytes); - chunk_typed_array(requests, cuopt::remote::FIELD_C, obj, upload_id, chunk_size_bytes); - chunk_typed_array( - requests, cuopt::remote::FIELD_VARIABLE_LOWER_BOUNDS, var_lb, upload_id, chunk_size_bytes); - chunk_typed_array( - requests, cuopt::remote::FIELD_VARIABLE_UPPER_BOUNDS, var_ub, upload_id, chunk_size_bytes); - chunk_typed_array( - requests, cuopt::remote::FIELD_CONSTRAINT_LOWER_BOUNDS, con_lb, upload_id, chunk_size_bytes); - chunk_typed_array( - requests, cuopt::remote::FIELD_CONSTRAINT_UPPER_BOUNDS, con_ub, upload_id, chunk_size_bytes); - chunk_typed_array(requests, cuopt::remote::FIELD_B, b, upload_id, chunk_size_bytes); - - auto row_types = problem.get_row_types_host(); - if (!row_types.empty()) { - std::vector rt_bytes(row_types.begin(), row_types.end()); - chunk_byte_blob( - requests, cuopt::remote::FIELD_ROW_TYPES, rt_bytes, upload_id, chunk_size_bytes); - } - - auto var_types = problem.get_variable_types_host(); - if (!var_types.empty()) { - std::vector vt_enums; - vt_enums.reserve(var_types.size()); - for (const auto& vt : var_types) { - switch (vt) { - case var_t::CONTINUOUS: vt_enums.push_back(cuopt::remote::CONTINUOUS); break; - case var_t::INTEGER: vt_enums.push_back(cuopt::remote::INTEGER); break; - default: - throw std::runtime_error("chunk_problem_to_proto: unknown var_t value " + - std::to_string(static_cast(vt))); - } - } - chunk_typed_array( - requests, cuopt::remote::FIELD_VARIABLE_TYPES, vt_enums, upload_id, chunk_size_bytes); - } - - if (problem.has_quadratic_objective()) { - const auto& q_values = problem.get_quadratic_objective_values(); - const auto& q_indices = problem.get_quadratic_objective_indices(); - const auto& q_offsets = problem.get_quadratic_objective_offsets(); - chunk_typed_array( - requests, cuopt::remote::FIELD_Q_VALUES, q_values, upload_id, chunk_size_bytes); - chunk_typed_array( - requests, cuopt::remote::FIELD_Q_INDICES, q_indices, upload_id, chunk_size_bytes); - chunk_typed_array( - requests, cuopt::remote::FIELD_Q_OFFSETS, q_offsets, upload_id, chunk_size_bytes); - } - - auto names_to_blob = [](const std::vector& names) -> std::vector { - if (names.empty()) return {}; - size_t total = 0; - for (const auto& n : names) - total += n.size() + 1; - std::vector blob(total); - size_t pos = 0; - for (const auto& n : names) { - std::memcpy(blob.data() + pos, n.data(), n.size()); - pos += n.size(); - blob[pos++] = '\0'; - } - return blob; - }; - - auto var_names_blob = names_to_blob(problem.get_variable_names()); - auto row_names_blob = names_to_blob(problem.get_row_names()); - chunk_byte_blob( - requests, cuopt::remote::FIELD_VARIABLE_NAMES, var_names_blob, upload_id, chunk_size_bytes); - chunk_byte_blob( - requests, cuopt::remote::FIELD_ROW_NAMES, row_names_blob, upload_id, chunk_size_bytes); - - return requests; +#include "generated_build_array_chunks.inc" } // Explicit template instantiations diff --git a/cpp/src/grpc/grpc_settings_mapper.cpp b/cpp/src/grpc/grpc_settings_mapper.cpp index 9b503b388e..1d341b6fc3 100644 --- a/cpp/src/grpc/grpc_settings_mapper.cpp +++ b/cpp/src/grpc/grpc_settings_mapper.cpp @@ -18,276 +18,35 @@ namespace cuopt::linear_programming { namespace { - -// Convert cuOpt pdlp_solver_mode_t to protobuf enum -cuopt::remote::PDLPSolverMode to_proto_pdlp_mode(pdlp_solver_mode_t mode) -{ - switch (mode) { - case pdlp_solver_mode_t::Stable1: return cuopt::remote::Stable1; - case pdlp_solver_mode_t::Stable2: return cuopt::remote::Stable2; - case pdlp_solver_mode_t::Methodical1: return cuopt::remote::Methodical1; - case pdlp_solver_mode_t::Fast1: return cuopt::remote::Fast1; - case pdlp_solver_mode_t::Stable3: return cuopt::remote::Stable3; - } - throw std::invalid_argument("Unknown pdlp_solver_mode_t: " + - std::to_string(static_cast(mode))); -} - -// Convert protobuf enum to cuOpt pdlp_solver_mode_t -pdlp_solver_mode_t from_proto_pdlp_mode(cuopt::remote::PDLPSolverMode mode) -{ - switch (mode) { - case cuopt::remote::Stable1: return pdlp_solver_mode_t::Stable1; - case cuopt::remote::Stable2: return pdlp_solver_mode_t::Stable2; - case cuopt::remote::Methodical1: return pdlp_solver_mode_t::Methodical1; - case cuopt::remote::Fast1: return pdlp_solver_mode_t::Fast1; - case cuopt::remote::Stable3: return pdlp_solver_mode_t::Stable3; - } - throw std::invalid_argument("Unknown PDLPSolverMode: " + std::to_string(static_cast(mode))); -} - -// Convert cuOpt method_t to protobuf enum -cuopt::remote::LPMethod to_proto_method(method_t method) -{ - switch (method) { - case method_t::Concurrent: return cuopt::remote::Concurrent; - case method_t::PDLP: return cuopt::remote::PDLP; - case method_t::DualSimplex: return cuopt::remote::DualSimplex; - case method_t::Barrier: return cuopt::remote::Barrier; - } - throw std::invalid_argument("Unknown method_t: " + std::to_string(static_cast(method))); -} - -// Convert protobuf enum to cuOpt method_t -method_t from_proto_method(cuopt::remote::LPMethod method) -{ - switch (method) { - case cuopt::remote::Concurrent: return method_t::Concurrent; - case cuopt::remote::PDLP: return method_t::PDLP; - case cuopt::remote::DualSimplex: return method_t::DualSimplex; - case cuopt::remote::Barrier: return method_t::Barrier; - } - throw std::invalid_argument("Unknown LPMethod: " + std::to_string(static_cast(method))); -} - -} // anonymous namespace +#include "generated_enum_converters_settings.inc" +} // namespace template void map_pdlp_settings_to_proto(const pdlp_solver_settings_t& settings, cuopt::remote::PDLPSolverSettings* pb_settings) { - // Termination tolerances (all names match cuOpt API) - pb_settings->set_absolute_gap_tolerance(settings.tolerances.absolute_gap_tolerance); - pb_settings->set_relative_gap_tolerance(settings.tolerances.relative_gap_tolerance); - pb_settings->set_primal_infeasible_tolerance(settings.tolerances.primal_infeasible_tolerance); - pb_settings->set_dual_infeasible_tolerance(settings.tolerances.dual_infeasible_tolerance); - pb_settings->set_absolute_dual_tolerance(settings.tolerances.absolute_dual_tolerance); - pb_settings->set_relative_dual_tolerance(settings.tolerances.relative_dual_tolerance); - pb_settings->set_absolute_primal_tolerance(settings.tolerances.absolute_primal_tolerance); - pb_settings->set_relative_primal_tolerance(settings.tolerances.relative_primal_tolerance); - - // Limits - pb_settings->set_time_limit(settings.time_limit); - // Avoid emitting a huge number when the iteration limit is the library default. - // Use -1 sentinel for "unset/use server defaults". - if (settings.iteration_limit == std::numeric_limits::max()) { - pb_settings->set_iteration_limit(-1); - } else { - pb_settings->set_iteration_limit(static_cast(settings.iteration_limit)); - } - - // Solver configuration - pb_settings->set_log_to_console(settings.log_to_console); - pb_settings->set_detect_infeasibility(settings.detect_infeasibility); - pb_settings->set_strict_infeasibility(settings.strict_infeasibility); - pb_settings->set_pdlp_solver_mode(to_proto_pdlp_mode(settings.pdlp_solver_mode)); - pb_settings->set_method(to_proto_method(settings.method)); - pb_settings->set_presolver(static_cast(settings.presolver)); - pb_settings->set_dual_postsolve(settings.dual_postsolve); - pb_settings->set_crossover(settings.crossover); - pb_settings->set_num_gpus(settings.num_gpus); - - pb_settings->set_per_constraint_residual(settings.per_constraint_residual); - pb_settings->set_cudss_deterministic(settings.cudss_deterministic); - pb_settings->set_folding(settings.folding); - pb_settings->set_augmented(settings.augmented); - pb_settings->set_dualize(settings.dualize); - pb_settings->set_ordering(settings.ordering); - pb_settings->set_barrier_dual_initial_point(settings.barrier_dual_initial_point); - pb_settings->set_eliminate_dense_columns(settings.eliminate_dense_columns); - pb_settings->set_pdlp_precision(static_cast(settings.pdlp_precision)); - pb_settings->set_save_best_primal_so_far(settings.save_best_primal_so_far); - pb_settings->set_first_primal_feasible(settings.first_primal_feasible); +#include "generated_pdlp_settings_to_proto.inc" } template void map_proto_to_pdlp_settings(const cuopt::remote::PDLPSolverSettings& pb_settings, pdlp_solver_settings_t& settings) { - // Termination tolerances (all names match cuOpt API) - settings.tolerances.absolute_gap_tolerance = pb_settings.absolute_gap_tolerance(); - settings.tolerances.relative_gap_tolerance = pb_settings.relative_gap_tolerance(); - settings.tolerances.primal_infeasible_tolerance = pb_settings.primal_infeasible_tolerance(); - settings.tolerances.dual_infeasible_tolerance = pb_settings.dual_infeasible_tolerance(); - settings.tolerances.absolute_dual_tolerance = pb_settings.absolute_dual_tolerance(); - settings.tolerances.relative_dual_tolerance = pb_settings.relative_dual_tolerance(); - settings.tolerances.absolute_primal_tolerance = pb_settings.absolute_primal_tolerance(); - settings.tolerances.relative_primal_tolerance = pb_settings.relative_primal_tolerance(); - - // Limits - settings.time_limit = pb_settings.time_limit(); - // proto3 defaults numeric fields to 0; treat negative iteration_limit as "unset" - // so the server keeps the library default (typically max()). - if (pb_settings.iteration_limit() >= 0) { - const auto limit = pb_settings.iteration_limit(); - settings.iteration_limit = (limit > static_cast(std::numeric_limits::max())) - ? std::numeric_limits::max() - : static_cast(limit); - } - - // Solver configuration - settings.log_to_console = pb_settings.log_to_console(); - settings.detect_infeasibility = pb_settings.detect_infeasibility(); - settings.strict_infeasibility = pb_settings.strict_infeasibility(); - settings.pdlp_solver_mode = from_proto_pdlp_mode(pb_settings.pdlp_solver_mode()); - settings.method = from_proto_method(pb_settings.method()); - { - auto pv = pb_settings.presolver(); - settings.presolver = (pv >= CUOPT_PRESOLVE_DEFAULT && pv <= CUOPT_PRESOLVE_PSLP) - ? static_cast(pv) - : presolver_t::Default; - } - settings.dual_postsolve = pb_settings.dual_postsolve(); - settings.crossover = pb_settings.crossover(); - settings.num_gpus = pb_settings.num_gpus(); - - settings.per_constraint_residual = pb_settings.per_constraint_residual(); - settings.cudss_deterministic = pb_settings.cudss_deterministic(); - settings.folding = pb_settings.folding(); - settings.augmented = pb_settings.augmented(); - settings.dualize = pb_settings.dualize(); - settings.ordering = pb_settings.ordering(); - settings.barrier_dual_initial_point = pb_settings.barrier_dual_initial_point(); - settings.eliminate_dense_columns = pb_settings.eliminate_dense_columns(); - { - auto pv = pb_settings.pdlp_precision(); - settings.pdlp_precision = - (pv >= CUOPT_PDLP_DEFAULT_PRECISION && pv <= CUOPT_PDLP_MIXED_PRECISION) - ? static_cast(pv) - : pdlp_precision_t::DefaultPrecision; - } - settings.save_best_primal_so_far = pb_settings.save_best_primal_so_far(); - settings.first_primal_feasible = pb_settings.first_primal_feasible(); +#include "generated_proto_to_pdlp_settings.inc" } template void map_mip_settings_to_proto(const mip_solver_settings_t& settings, cuopt::remote::MIPSolverSettings* pb_settings) { - // Limits - pb_settings->set_time_limit(settings.time_limit); - - // Tolerances (all names match cuOpt API) - pb_settings->set_relative_mip_gap(settings.tolerances.relative_mip_gap); - pb_settings->set_absolute_mip_gap(settings.tolerances.absolute_mip_gap); - pb_settings->set_integrality_tolerance(settings.tolerances.integrality_tolerance); - pb_settings->set_absolute_tolerance(settings.tolerances.absolute_tolerance); - pb_settings->set_relative_tolerance(settings.tolerances.relative_tolerance); - pb_settings->set_presolve_absolute_tolerance(settings.tolerances.presolve_absolute_tolerance); - - // Solver configuration - pb_settings->set_log_to_console(settings.log_to_console); - pb_settings->set_heuristics_only(settings.heuristics_only); - pb_settings->set_num_cpu_threads(settings.num_cpu_threads); - pb_settings->set_num_gpus(settings.num_gpus); - pb_settings->set_presolver(static_cast(settings.presolver)); - pb_settings->set_mip_scaling(settings.mip_scaling); - - // Additional limits - pb_settings->set_work_limit(settings.work_limit); - if (settings.node_limit == std::numeric_limits::max()) { - pb_settings->set_node_limit(-1); - } else { - pb_settings->set_node_limit(static_cast(settings.node_limit)); - } - - // Branching - pb_settings->set_reliability_branching(settings.reliability_branching); - pb_settings->set_mip_batch_pdlp_strong_branching(settings.mip_batch_pdlp_strong_branching); - - // Cut configuration - pb_settings->set_max_cut_passes(settings.max_cut_passes); - pb_settings->set_mir_cuts(settings.mir_cuts); - pb_settings->set_mixed_integer_gomory_cuts(settings.mixed_integer_gomory_cuts); - pb_settings->set_knapsack_cuts(settings.knapsack_cuts); - pb_settings->set_clique_cuts(settings.clique_cuts); - pb_settings->set_strong_chvatal_gomory_cuts(settings.strong_chvatal_gomory_cuts); - pb_settings->set_reduced_cost_strengthening(settings.reduced_cost_strengthening); - pb_settings->set_cut_change_threshold(settings.cut_change_threshold); - pb_settings->set_cut_min_orthogonality(settings.cut_min_orthogonality); - - // Determinism and reproducibility - pb_settings->set_determinism_mode(settings.determinism_mode); - pb_settings->set_seed(settings.seed); +#include "generated_mip_settings_to_proto.inc" } template void map_proto_to_mip_settings(const cuopt::remote::MIPSolverSettings& pb_settings, mip_solver_settings_t& settings) { - // Limits - settings.time_limit = pb_settings.time_limit(); - - // Tolerances (all names match cuOpt API) - settings.tolerances.relative_mip_gap = pb_settings.relative_mip_gap(); - settings.tolerances.absolute_mip_gap = pb_settings.absolute_mip_gap(); - settings.tolerances.integrality_tolerance = pb_settings.integrality_tolerance(); - settings.tolerances.absolute_tolerance = pb_settings.absolute_tolerance(); - settings.tolerances.relative_tolerance = pb_settings.relative_tolerance(); - settings.tolerances.presolve_absolute_tolerance = pb_settings.presolve_absolute_tolerance(); - - // Solver configuration - settings.log_to_console = pb_settings.log_to_console(); - settings.heuristics_only = pb_settings.heuristics_only(); - settings.num_cpu_threads = pb_settings.num_cpu_threads(); - settings.num_gpus = pb_settings.num_gpus(); - { - auto pv = pb_settings.presolver(); - settings.presolver = (pv >= CUOPT_PRESOLVE_DEFAULT && pv <= CUOPT_PRESOLVE_PSLP) - ? static_cast(pv) - : presolver_t::Default; - } - { - auto sv = pb_settings.mip_scaling(); - settings.mip_scaling = (sv >= CUOPT_MIP_SCALING_OFF && sv <= CUOPT_MIP_SCALING_NO_OBJECTIVE) - ? sv - : CUOPT_MIP_SCALING_ON; - } - - // Additional limits - settings.work_limit = pb_settings.work_limit(); - if (pb_settings.node_limit() >= 0) { - settings.node_limit = static_cast(pb_settings.node_limit()); - } - - // Branching - settings.reliability_branching = pb_settings.reliability_branching(); - settings.mip_batch_pdlp_strong_branching = pb_settings.mip_batch_pdlp_strong_branching(); - - // Cut configuration - settings.max_cut_passes = pb_settings.max_cut_passes(); - settings.mir_cuts = pb_settings.mir_cuts(); - settings.mixed_integer_gomory_cuts = pb_settings.mixed_integer_gomory_cuts(); - settings.knapsack_cuts = pb_settings.knapsack_cuts(); - settings.clique_cuts = pb_settings.clique_cuts(); - settings.strong_chvatal_gomory_cuts = pb_settings.strong_chvatal_gomory_cuts(); - settings.reduced_cost_strengthening = pb_settings.reduced_cost_strengthening(); - settings.cut_change_threshold = pb_settings.cut_change_threshold(); - settings.cut_min_orthogonality = pb_settings.cut_min_orthogonality(); - - // Determinism and reproducibility - settings.determinism_mode = pb_settings.determinism_mode(); - settings.seed = pb_settings.seed(); +#include "generated_proto_to_mip_settings.inc" } // Explicit template instantiations diff --git a/cpp/src/grpc/grpc_solution_mapper.cpp b/cpp/src/grpc/grpc_solution_mapper.cpp index 3be106cdca..5f2acfa595 100644 --- a/cpp/src/grpc/grpc_solution_mapper.cpp +++ b/cpp/src/grpc/grpc_solution_mapper.cpp @@ -17,265 +17,86 @@ namespace cuopt::linear_programming { -// Convert cuOpt termination status to protobuf enum -cuopt::remote::PDLPTerminationStatus to_proto_pdlp_status(pdlp_termination_status_t status) -{ - switch (status) { - case pdlp_termination_status_t::NoTermination: return cuopt::remote::PDLP_NO_TERMINATION; - case pdlp_termination_status_t::NumericalError: return cuopt::remote::PDLP_NUMERICAL_ERROR; - case pdlp_termination_status_t::Optimal: return cuopt::remote::PDLP_OPTIMAL; - case pdlp_termination_status_t::PrimalInfeasible: return cuopt::remote::PDLP_PRIMAL_INFEASIBLE; - case pdlp_termination_status_t::DualInfeasible: return cuopt::remote::PDLP_DUAL_INFEASIBLE; - case pdlp_termination_status_t::IterationLimit: return cuopt::remote::PDLP_ITERATION_LIMIT; - case pdlp_termination_status_t::TimeLimit: return cuopt::remote::PDLP_TIME_LIMIT; - case pdlp_termination_status_t::ConcurrentLimit: return cuopt::remote::PDLP_CONCURRENT_LIMIT; - case pdlp_termination_status_t::PrimalFeasible: return cuopt::remote::PDLP_PRIMAL_FEASIBLE; - default: return cuopt::remote::PDLP_NO_TERMINATION; - } -} +namespace { +#include "generated_enum_converters_solution.inc" -// Convert protobuf enum to cuOpt termination status -pdlp_termination_status_t from_proto_pdlp_status(cuopt::remote::PDLPTerminationStatus status) +void add_result_array_descriptor(cuopt::remote::ChunkedResultHeader* header, + cuopt::remote::ResultFieldId fid, + int64_t count, + int64_t elem_size) { - switch (status) { - case cuopt::remote::PDLP_NO_TERMINATION: return pdlp_termination_status_t::NoTermination; - case cuopt::remote::PDLP_NUMERICAL_ERROR: return pdlp_termination_status_t::NumericalError; - case cuopt::remote::PDLP_OPTIMAL: return pdlp_termination_status_t::Optimal; - case cuopt::remote::PDLP_PRIMAL_INFEASIBLE: return pdlp_termination_status_t::PrimalInfeasible; - case cuopt::remote::PDLP_DUAL_INFEASIBLE: return pdlp_termination_status_t::DualInfeasible; - case cuopt::remote::PDLP_ITERATION_LIMIT: return pdlp_termination_status_t::IterationLimit; - case cuopt::remote::PDLP_TIME_LIMIT: return pdlp_termination_status_t::TimeLimit; - case cuopt::remote::PDLP_CONCURRENT_LIMIT: return pdlp_termination_status_t::ConcurrentLimit; - case cuopt::remote::PDLP_PRIMAL_FEASIBLE: return pdlp_termination_status_t::PrimalFeasible; - default: return pdlp_termination_status_t::NoTermination; - } + if (count <= 0) return; + auto* desc = header->add_arrays(); + desc->set_field_id(fid); + desc->set_total_elements(count); + desc->set_element_size_bytes(elem_size); } -// Convert MIP termination status -cuopt::remote::MIPTerminationStatus to_proto_mip_status(mip_termination_status_t status) +template +std::vector doubles_to_bytes(const std::vector& vec) { - switch (status) { - case mip_termination_status_t::NoTermination: return cuopt::remote::MIP_NO_TERMINATION; - case mip_termination_status_t::Optimal: return cuopt::remote::MIP_OPTIMAL; - case mip_termination_status_t::FeasibleFound: return cuopt::remote::MIP_FEASIBLE_FOUND; - case mip_termination_status_t::Infeasible: return cuopt::remote::MIP_INFEASIBLE; - case mip_termination_status_t::Unbounded: return cuopt::remote::MIP_UNBOUNDED; - case mip_termination_status_t::TimeLimit: return cuopt::remote::MIP_TIME_LIMIT; - case mip_termination_status_t::WorkLimit: return cuopt::remote::MIP_WORK_LIMIT; - default: return cuopt::remote::MIP_NO_TERMINATION; - } + std::vector tmp(vec.begin(), vec.end()); + std::vector bytes(tmp.size() * sizeof(double)); + std::memcpy(bytes.data(), tmp.data(), bytes.size()); + return bytes; } -mip_termination_status_t from_proto_mip_status(cuopt::remote::MIPTerminationStatus status) +template +std::vector bytes_to_typed(const std::map>& arrays, + int32_t field_id) { - switch (status) { - case cuopt::remote::MIP_NO_TERMINATION: return mip_termination_status_t::NoTermination; - case cuopt::remote::MIP_OPTIMAL: return mip_termination_status_t::Optimal; - case cuopt::remote::MIP_FEASIBLE_FOUND: return mip_termination_status_t::FeasibleFound; - case cuopt::remote::MIP_INFEASIBLE: return mip_termination_status_t::Infeasible; - case cuopt::remote::MIP_UNBOUNDED: return mip_termination_status_t::Unbounded; - case cuopt::remote::MIP_TIME_LIMIT: return mip_termination_status_t::TimeLimit; - case cuopt::remote::MIP_WORK_LIMIT: return mip_termination_status_t::WorkLimit; - default: return mip_termination_status_t::NoTermination; + auto it = arrays.find(field_id); + if (it == arrays.end() || it->second.empty()) return {}; + + const auto& raw = it->second; + if constexpr (std::is_same_v) { + if (raw.size() % sizeof(double) != 0) return {}; + size_t n = raw.size() / sizeof(double); + std::vector tmp(n); + std::memcpy(tmp.data(), raw.data(), n * sizeof(double)); + return std::vector(tmp.begin(), tmp.end()); + } else if constexpr (std::is_same_v) { + if (raw.size() % sizeof(double) != 0) return {}; + size_t n = raw.size() / sizeof(double); + std::vector v(n); + std::memcpy(v.data(), raw.data(), n * sizeof(double)); + return v; + } else { + if (raw.size() % sizeof(T) != 0) return {}; + size_t n = raw.size() / sizeof(T); + std::vector v(n); + std::memcpy(v.data(), raw.data(), n * sizeof(T)); + return v; } } +} // namespace + template void map_lp_solution_to_proto(const cpu_lp_solution_t& solution, cuopt::remote::LPSolution* pb_solution) { - pb_solution->set_lp_termination_status(to_proto_pdlp_status(solution.get_termination_status())); - pb_solution->set_error_message(solution.get_error_status().what()); - - // Solution vectors - CPU solution already has data in host memory - const auto& primal = solution.get_primal_solution_host(); - const auto& dual = solution.get_dual_solution_host(); - const auto& reduced_cost = solution.get_reduced_cost_host(); - - for (const auto& v : primal) { - pb_solution->add_primal_solution(static_cast(v)); - } - for (const auto& v : dual) { - pb_solution->add_dual_solution(static_cast(v)); - } - for (const auto& v : reduced_cost) { - pb_solution->add_reduced_cost(static_cast(v)); - } - - // Statistics - pb_solution->set_l2_primal_residual(solution.get_l2_primal_residual()); - pb_solution->set_l2_dual_residual(solution.get_l2_dual_residual()); - pb_solution->set_primal_objective(solution.get_objective_value()); - pb_solution->set_dual_objective(solution.get_dual_objective_value()); - pb_solution->set_gap(solution.get_gap()); - pb_solution->set_nb_iterations(solution.get_num_iterations()); - pb_solution->set_solve_time(solution.get_solve_time()); - pb_solution->set_solved_by(static_cast(solution.solved_by())); - - if (solution.has_warm_start_data()) { - auto* pb_ws = pb_solution->mutable_warm_start_data(); - const auto& ws = solution.get_cpu_pdlp_warm_start_data(); - - for (const auto& v : ws.current_primal_solution_) - pb_ws->add_current_primal_solution(static_cast(v)); - for (const auto& v : ws.current_dual_solution_) - pb_ws->add_current_dual_solution(static_cast(v)); - for (const auto& v : ws.initial_primal_average_) - pb_ws->add_initial_primal_average(static_cast(v)); - for (const auto& v : ws.initial_dual_average_) - pb_ws->add_initial_dual_average(static_cast(v)); - for (const auto& v : ws.current_ATY_) - pb_ws->add_current_aty(static_cast(v)); - for (const auto& v : ws.sum_primal_solutions_) - pb_ws->add_sum_primal_solutions(static_cast(v)); - for (const auto& v : ws.sum_dual_solutions_) - pb_ws->add_sum_dual_solutions(static_cast(v)); - for (const auto& v : ws.last_restart_duality_gap_primal_solution_) - pb_ws->add_last_restart_duality_gap_primal_solution(static_cast(v)); - for (const auto& v : ws.last_restart_duality_gap_dual_solution_) - pb_ws->add_last_restart_duality_gap_dual_solution(static_cast(v)); - - pb_ws->set_initial_primal_weight(static_cast(ws.initial_primal_weight_)); - pb_ws->set_initial_step_size(static_cast(ws.initial_step_size_)); - pb_ws->set_total_pdlp_iterations(static_cast(ws.total_pdlp_iterations_)); - pb_ws->set_total_pdhg_iterations(static_cast(ws.total_pdhg_iterations_)); - pb_ws->set_last_candidate_kkt_score(static_cast(ws.last_candidate_kkt_score_)); - pb_ws->set_last_restart_kkt_score(static_cast(ws.last_restart_kkt_score_)); - pb_ws->set_sum_solution_weight(static_cast(ws.sum_solution_weight_)); - pb_ws->set_iterations_since_last_restart( - static_cast(ws.iterations_since_last_restart_)); - } +#include "generated_lp_solution_to_proto.inc" } template cpu_lp_solution_t map_proto_to_lp_solution(const cuopt::remote::LPSolution& pb_solution) { - // Convert solution vectors - std::vector primal(pb_solution.primal_solution().begin(), - pb_solution.primal_solution().end()); - std::vector dual(pb_solution.dual_solution().begin(), pb_solution.dual_solution().end()); - std::vector reduced_cost(pb_solution.reduced_cost().begin(), - pb_solution.reduced_cost().end()); - - auto status = from_proto_pdlp_status(pb_solution.lp_termination_status()); - auto obj = static_cast(pb_solution.primal_objective()); - auto dual_obj = static_cast(pb_solution.dual_objective()); - auto solve_t = pb_solution.solve_time(); - auto l2_pr = static_cast(pb_solution.l2_primal_residual()); - auto l2_dr = static_cast(pb_solution.l2_dual_residual()); - auto g = static_cast(pb_solution.gap()); - auto iters = static_cast(pb_solution.nb_iterations()); - auto solved_by = static_cast(pb_solution.solved_by()); - - if (pb_solution.has_warm_start_data()) { - const auto& pb_ws = pb_solution.warm_start_data(); - cpu_pdlp_warm_start_data_t ws; - - ws.current_primal_solution_.assign(pb_ws.current_primal_solution().begin(), - pb_ws.current_primal_solution().end()); - ws.current_dual_solution_.assign(pb_ws.current_dual_solution().begin(), - pb_ws.current_dual_solution().end()); - ws.initial_primal_average_.assign(pb_ws.initial_primal_average().begin(), - pb_ws.initial_primal_average().end()); - ws.initial_dual_average_.assign(pb_ws.initial_dual_average().begin(), - pb_ws.initial_dual_average().end()); - ws.current_ATY_.assign(pb_ws.current_aty().begin(), pb_ws.current_aty().end()); - ws.sum_primal_solutions_.assign(pb_ws.sum_primal_solutions().begin(), - pb_ws.sum_primal_solutions().end()); - ws.sum_dual_solutions_.assign(pb_ws.sum_dual_solutions().begin(), - pb_ws.sum_dual_solutions().end()); - ws.last_restart_duality_gap_primal_solution_.assign( - pb_ws.last_restart_duality_gap_primal_solution().begin(), - pb_ws.last_restart_duality_gap_primal_solution().end()); - ws.last_restart_duality_gap_dual_solution_.assign( - pb_ws.last_restart_duality_gap_dual_solution().begin(), - pb_ws.last_restart_duality_gap_dual_solution().end()); - - ws.initial_primal_weight_ = static_cast(pb_ws.initial_primal_weight()); - ws.initial_step_size_ = static_cast(pb_ws.initial_step_size()); - ws.total_pdlp_iterations_ = static_cast(pb_ws.total_pdlp_iterations()); - ws.total_pdhg_iterations_ = static_cast(pb_ws.total_pdhg_iterations()); - ws.last_candidate_kkt_score_ = static_cast(pb_ws.last_candidate_kkt_score()); - ws.last_restart_kkt_score_ = static_cast(pb_ws.last_restart_kkt_score()); - ws.sum_solution_weight_ = static_cast(pb_ws.sum_solution_weight()); - ws.iterations_since_last_restart_ = static_cast(pb_ws.iterations_since_last_restart()); - - return cpu_lp_solution_t(std::move(primal), - std::move(dual), - std::move(reduced_cost), - status, - obj, - dual_obj, - solve_t, - l2_pr, - l2_dr, - g, - iters, - solved_by, - std::move(ws)); - } - - return cpu_lp_solution_t(std::move(primal), - std::move(dual), - std::move(reduced_cost), - status, - obj, - dual_obj, - solve_t, - l2_pr, - l2_dr, - g, - iters, - solved_by); +#include "generated_proto_to_lp_solution.inc" } template void map_mip_solution_to_proto(const cpu_mip_solution_t& solution, cuopt::remote::MIPSolution* pb_solution) { - pb_solution->set_mip_termination_status(to_proto_mip_status(solution.get_termination_status())); - pb_solution->set_mip_error_message(solution.get_error_status().what()); - - // Solution vector - CPU solution already has data in host memory - const auto& sol_vec = solution.get_solution_host(); - for (const auto& v : sol_vec) { - pb_solution->add_mip_solution(static_cast(v)); - } - - // Solution statistics - pb_solution->set_mip_objective(solution.get_objective_value()); - pb_solution->set_mip_gap(solution.get_mip_gap()); - pb_solution->set_solution_bound(solution.get_solution_bound()); - pb_solution->set_total_solve_time(solution.get_solve_time()); - pb_solution->set_presolve_time(solution.get_presolve_time()); - pb_solution->set_max_constraint_violation(solution.get_max_constraint_violation()); - pb_solution->set_max_int_violation(solution.get_max_int_violation()); - pb_solution->set_max_variable_bound_violation(solution.get_max_variable_bound_violation()); - pb_solution->set_nodes(solution.get_num_nodes()); - pb_solution->set_simplex_iterations(solution.get_num_simplex_iterations()); +#include "generated_mip_solution_to_proto.inc" } template cpu_mip_solution_t map_proto_to_mip_solution( const cuopt::remote::MIPSolution& pb_solution) { - // Convert solution vector - std::vector solution_vec(pb_solution.mip_solution().begin(), - pb_solution.mip_solution().end()); - - // Create CPU MIP solution with data - return cpu_mip_solution_t(std::move(solution_vec), - from_proto_mip_status(pb_solution.mip_termination_status()), - static_cast(pb_solution.mip_objective()), - static_cast(pb_solution.mip_gap()), - static_cast(pb_solution.solution_bound()), - pb_solution.total_solve_time(), - pb_solution.presolve_time(), - static_cast(pb_solution.max_constraint_violation()), - static_cast(pb_solution.max_int_violation()), - static_cast(pb_solution.max_variable_bound_violation()), - static_cast(pb_solution.nodes()), - static_cast(pb_solution.simplex_iterations())); +#include "generated_proto_to_mip_solution.inc" } // ============================================================================ @@ -285,156 +106,31 @@ cpu_mip_solution_t map_proto_to_mip_solution( template size_t estimate_lp_solution_proto_size(const cpu_lp_solution_t& solution) { - size_t est = 0; - est += static_cast(solution.get_primal_solution_size()) * sizeof(double); - est += static_cast(solution.get_dual_solution_size()) * sizeof(double); - est += static_cast(solution.get_reduced_cost_size()) * sizeof(double); - if (solution.has_warm_start_data()) { - const auto& ws = solution.get_cpu_pdlp_warm_start_data(); - est += ws.current_primal_solution_.size() * sizeof(double); - est += ws.current_dual_solution_.size() * sizeof(double); - est += ws.initial_primal_average_.size() * sizeof(double); - est += ws.initial_dual_average_.size() * sizeof(double); - est += ws.current_ATY_.size() * sizeof(double); - est += ws.sum_primal_solutions_.size() * sizeof(double); - est += ws.sum_dual_solutions_.size() * sizeof(double); - est += ws.last_restart_duality_gap_primal_solution_.size() * sizeof(double); - est += ws.last_restart_duality_gap_dual_solution_.size() * sizeof(double); - } - est += 512; // scalars + tags overhead - return est; +#include "generated_estimate_lp_size.inc" } template size_t estimate_mip_solution_proto_size(const cpu_mip_solution_t& solution) { - size_t est = 0; - est += static_cast(solution.get_solution_size()) * sizeof(double); - est += 256; // scalars + tags overhead - return est; +#include "generated_estimate_mip_size.inc" } // ============================================================================ // Chunked result header population // ============================================================================ -namespace { -void add_result_array_descriptor(cuopt::remote::ChunkedResultHeader* header, - cuopt::remote::ResultFieldId fid, - int64_t count, - int64_t elem_size) -{ - if (count <= 0) return; - auto* desc = header->add_arrays(); - desc->set_field_id(fid); - desc->set_total_elements(count); - desc->set_element_size_bytes(elem_size); -} - -template -std::vector doubles_to_bytes(const std::vector& vec) -{ - std::vector tmp(vec.begin(), vec.end()); - std::vector bytes(tmp.size() * sizeof(double)); - std::memcpy(bytes.data(), tmp.data(), bytes.size()); - return bytes; -} -} // namespace - template void populate_chunked_result_header_lp(const cpu_lp_solution_t& solution, cuopt::remote::ChunkedResultHeader* header) { - header->set_problem_category(cuopt::remote::LP); - header->set_lp_termination_status(to_proto_pdlp_status(solution.get_termination_status())); - header->set_error_message(solution.get_error_status().what()); - header->set_l2_primal_residual(solution.get_l2_primal_residual()); - header->set_l2_dual_residual(solution.get_l2_dual_residual()); - header->set_primal_objective(solution.get_objective_value()); - header->set_dual_objective(solution.get_dual_objective_value()); - header->set_gap(solution.get_gap()); - header->set_nb_iterations(solution.get_num_iterations()); - header->set_solve_time(solution.get_solve_time()); - header->set_solved_by(static_cast(solution.solved_by())); - - const auto& primal = solution.get_primal_solution_host(); - const auto& dual = solution.get_dual_solution_host(); - const auto& reduced_cost = solution.get_reduced_cost_host(); - - add_result_array_descriptor( - header, cuopt::remote::RESULT_PRIMAL_SOLUTION, primal.size(), sizeof(double)); - add_result_array_descriptor( - header, cuopt::remote::RESULT_DUAL_SOLUTION, dual.size(), sizeof(double)); - add_result_array_descriptor( - header, cuopt::remote::RESULT_REDUCED_COST, reduced_cost.size(), sizeof(double)); - - if (solution.has_warm_start_data()) { - const auto& ws = solution.get_cpu_pdlp_warm_start_data(); - header->set_ws_initial_primal_weight(static_cast(ws.initial_primal_weight_)); - header->set_ws_initial_step_size(static_cast(ws.initial_step_size_)); - header->set_ws_total_pdlp_iterations(static_cast(ws.total_pdlp_iterations_)); - header->set_ws_total_pdhg_iterations(static_cast(ws.total_pdhg_iterations_)); - header->set_ws_last_candidate_kkt_score(static_cast(ws.last_candidate_kkt_score_)); - header->set_ws_last_restart_kkt_score(static_cast(ws.last_restart_kkt_score_)); - header->set_ws_sum_solution_weight(static_cast(ws.sum_solution_weight_)); - header->set_ws_iterations_since_last_restart( - static_cast(ws.iterations_since_last_restart_)); - - add_result_array_descriptor(header, - cuopt::remote::RESULT_WS_CURRENT_PRIMAL, - ws.current_primal_solution_.size(), - sizeof(double)); - add_result_array_descriptor(header, - cuopt::remote::RESULT_WS_CURRENT_DUAL, - ws.current_dual_solution_.size(), - sizeof(double)); - add_result_array_descriptor(header, - cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVG, - ws.initial_primal_average_.size(), - sizeof(double)); - add_result_array_descriptor(header, - cuopt::remote::RESULT_WS_INITIAL_DUAL_AVG, - ws.initial_dual_average_.size(), - sizeof(double)); - add_result_array_descriptor( - header, cuopt::remote::RESULT_WS_CURRENT_ATY, ws.current_ATY_.size(), sizeof(double)); - add_result_array_descriptor( - header, cuopt::remote::RESULT_WS_SUM_PRIMAL, ws.sum_primal_solutions_.size(), sizeof(double)); - add_result_array_descriptor( - header, cuopt::remote::RESULT_WS_SUM_DUAL, ws.sum_dual_solutions_.size(), sizeof(double)); - add_result_array_descriptor(header, - cuopt::remote::RESULT_WS_LAST_RESTART_GAP_PRIMAL, - ws.last_restart_duality_gap_primal_solution_.size(), - sizeof(double)); - add_result_array_descriptor(header, - cuopt::remote::RESULT_WS_LAST_RESTART_GAP_DUAL, - ws.last_restart_duality_gap_dual_solution_.size(), - sizeof(double)); - } +#include "generated_lp_chunked_header.inc" } template void populate_chunked_result_header_mip(const cpu_mip_solution_t& solution, cuopt::remote::ChunkedResultHeader* header) { - header->set_problem_category(cuopt::remote::MIP); - header->set_mip_termination_status(to_proto_mip_status(solution.get_termination_status())); - header->set_mip_error_message(solution.get_error_status().what()); - header->set_mip_objective(solution.get_objective_value()); - header->set_mip_gap(solution.get_mip_gap()); - header->set_solution_bound(solution.get_solution_bound()); - header->set_total_solve_time(solution.get_solve_time()); - header->set_presolve_time(solution.get_presolve_time()); - header->set_max_constraint_violation(solution.get_max_constraint_violation()); - header->set_max_int_violation(solution.get_max_int_violation()); - header->set_max_variable_bound_violation(solution.get_max_variable_bound_violation()); - header->set_nodes(solution.get_num_nodes()); - header->set_simplex_iterations(solution.get_num_simplex_iterations()); - - add_result_array_descriptor(header, - cuopt::remote::RESULT_MIP_SOLUTION, - solution.get_solution_host().size(), - sizeof(double)); +#include "generated_mip_chunked_header.inc" } // ============================================================================ @@ -445,176 +141,26 @@ template std::map> collect_lp_solution_arrays( const cpu_lp_solution_t& solution) { - std::map> arrays; - - const auto& primal = solution.get_primal_solution_host(); - const auto& dual = solution.get_dual_solution_host(); - const auto& reduced_cost = solution.get_reduced_cost_host(); - - if (!primal.empty()) { arrays[cuopt::remote::RESULT_PRIMAL_SOLUTION] = doubles_to_bytes(primal); } - if (!dual.empty()) { arrays[cuopt::remote::RESULT_DUAL_SOLUTION] = doubles_to_bytes(dual); } - if (!reduced_cost.empty()) { - arrays[cuopt::remote::RESULT_REDUCED_COST] = doubles_to_bytes(reduced_cost); - } - - if (solution.has_warm_start_data()) { - const auto& ws = solution.get_cpu_pdlp_warm_start_data(); - if (!ws.current_primal_solution_.empty()) { - arrays[cuopt::remote::RESULT_WS_CURRENT_PRIMAL] = - doubles_to_bytes(ws.current_primal_solution_); - } - if (!ws.current_dual_solution_.empty()) { - arrays[cuopt::remote::RESULT_WS_CURRENT_DUAL] = doubles_to_bytes(ws.current_dual_solution_); - } - if (!ws.initial_primal_average_.empty()) { - arrays[cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVG] = - doubles_to_bytes(ws.initial_primal_average_); - } - if (!ws.initial_dual_average_.empty()) { - arrays[cuopt::remote::RESULT_WS_INITIAL_DUAL_AVG] = - doubles_to_bytes(ws.initial_dual_average_); - } - if (!ws.current_ATY_.empty()) { - arrays[cuopt::remote::RESULT_WS_CURRENT_ATY] = doubles_to_bytes(ws.current_ATY_); - } - if (!ws.sum_primal_solutions_.empty()) { - arrays[cuopt::remote::RESULT_WS_SUM_PRIMAL] = doubles_to_bytes(ws.sum_primal_solutions_); - } - if (!ws.sum_dual_solutions_.empty()) { - arrays[cuopt::remote::RESULT_WS_SUM_DUAL] = doubles_to_bytes(ws.sum_dual_solutions_); - } - if (!ws.last_restart_duality_gap_primal_solution_.empty()) { - arrays[cuopt::remote::RESULT_WS_LAST_RESTART_GAP_PRIMAL] = - doubles_to_bytes(ws.last_restart_duality_gap_primal_solution_); - } - if (!ws.last_restart_duality_gap_dual_solution_.empty()) { - arrays[cuopt::remote::RESULT_WS_LAST_RESTART_GAP_DUAL] = - doubles_to_bytes(ws.last_restart_duality_gap_dual_solution_); - } - } - - return arrays; +#include "generated_collect_lp_arrays.inc" } template std::map> collect_mip_solution_arrays( const cpu_mip_solution_t& solution) { - std::map> arrays; - const auto& sol_vec = solution.get_solution_host(); - if (!sol_vec.empty()) { arrays[cuopt::remote::RESULT_MIP_SOLUTION] = doubles_to_bytes(sol_vec); } - return arrays; +#include "generated_collect_mip_arrays.inc" } // ============================================================================ // Chunked result -> solution (client-side) // ============================================================================ -namespace { - -template -std::vector bytes_to_typed(const std::map>& arrays, - int32_t field_id) -{ - auto it = arrays.find(field_id); - if (it == arrays.end() || it->second.empty()) return {}; - - const auto& raw = it->second; - if constexpr (std::is_same_v) { - if (raw.size() % sizeof(double) != 0) return {}; - size_t n = raw.size() / sizeof(double); - std::vector tmp(n); - std::memcpy(tmp.data(), raw.data(), n * sizeof(double)); - return std::vector(tmp.begin(), tmp.end()); - } else if constexpr (std::is_same_v) { - if (raw.size() % sizeof(double) != 0) return {}; - size_t n = raw.size() / sizeof(double); - std::vector v(n); - std::memcpy(v.data(), raw.data(), n * sizeof(double)); - return v; - } else { - if (raw.size() % sizeof(T) != 0) return {}; - size_t n = raw.size() / sizeof(T); - std::vector v(n); - std::memcpy(v.data(), raw.data(), n * sizeof(T)); - return v; - } -} - -} // namespace - template cpu_lp_solution_t chunked_result_to_lp_solution( const cuopt::remote::ChunkedResultHeader& h, const std::map>& arrays) { - auto primal = bytes_to_typed(arrays, cuopt::remote::RESULT_PRIMAL_SOLUTION); - auto dual = bytes_to_typed(arrays, cuopt::remote::RESULT_DUAL_SOLUTION); - auto reduced_cost = bytes_to_typed(arrays, cuopt::remote::RESULT_REDUCED_COST); - - auto status = from_proto_pdlp_status(h.lp_termination_status()); - auto obj = static_cast(h.primal_objective()); - auto dual_obj = static_cast(h.dual_objective()); - auto solve_t = h.solve_time(); - auto l2_pr = static_cast(h.l2_primal_residual()); - auto l2_dr = static_cast(h.l2_dual_residual()); - auto g = static_cast(h.gap()); - auto iters = static_cast(h.nb_iterations()); - auto solved_by = static_cast(h.solved_by()); - - auto ws_primal = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_PRIMAL); - if (!ws_primal.empty()) { - cpu_pdlp_warm_start_data_t ws; - ws.current_primal_solution_ = std::move(ws_primal); - ws.current_dual_solution_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_DUAL); - ws.initial_primal_average_ = - bytes_to_typed(arrays, cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVG); - ws.initial_dual_average_ = - bytes_to_typed(arrays, cuopt::remote::RESULT_WS_INITIAL_DUAL_AVG); - ws.current_ATY_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_ATY); - ws.sum_primal_solutions_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_SUM_PRIMAL); - ws.sum_dual_solutions_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_SUM_DUAL); - ws.last_restart_duality_gap_primal_solution_ = - bytes_to_typed(arrays, cuopt::remote::RESULT_WS_LAST_RESTART_GAP_PRIMAL); - ws.last_restart_duality_gap_dual_solution_ = - bytes_to_typed(arrays, cuopt::remote::RESULT_WS_LAST_RESTART_GAP_DUAL); - - ws.initial_primal_weight_ = static_cast(h.ws_initial_primal_weight()); - ws.initial_step_size_ = static_cast(h.ws_initial_step_size()); - ws.total_pdlp_iterations_ = static_cast(h.ws_total_pdlp_iterations()); - ws.total_pdhg_iterations_ = static_cast(h.ws_total_pdhg_iterations()); - ws.last_candidate_kkt_score_ = static_cast(h.ws_last_candidate_kkt_score()); - ws.last_restart_kkt_score_ = static_cast(h.ws_last_restart_kkt_score()); - ws.sum_solution_weight_ = static_cast(h.ws_sum_solution_weight()); - ws.iterations_since_last_restart_ = static_cast(h.ws_iterations_since_last_restart()); - - return cpu_lp_solution_t(std::move(primal), - std::move(dual), - std::move(reduced_cost), - status, - obj, - dual_obj, - solve_t, - l2_pr, - l2_dr, - g, - iters, - solved_by, - std::move(ws)); - } - - return cpu_lp_solution_t(std::move(primal), - std::move(dual), - std::move(reduced_cost), - status, - obj, - dual_obj, - solve_t, - l2_pr, - l2_dr, - g, - iters, - solved_by); +#include "generated_chunked_to_lp_solution.inc" } template @@ -622,20 +168,7 @@ cpu_mip_solution_t chunked_result_to_mip_solution( const cuopt::remote::ChunkedResultHeader& h, const std::map>& arrays) { - auto sol_vec = bytes_to_typed(arrays, cuopt::remote::RESULT_MIP_SOLUTION); - - return cpu_mip_solution_t(std::move(sol_vec), - from_proto_mip_status(h.mip_termination_status()), - static_cast(h.mip_objective()), - static_cast(h.mip_gap()), - static_cast(h.solution_bound()), - h.total_solve_time(), - h.presolve_time(), - static_cast(h.max_constraint_violation()), - static_cast(h.max_int_violation()), - static_cast(h.max_variable_bound_violation()), - static_cast(h.nodes()), - static_cast(h.simplex_iterations())); +#include "generated_chunked_to_mip_solution.inc" } // ============================================================================ diff --git a/cpp/src/grpc/grpc_solution_mapper.hpp b/cpp/src/grpc/grpc_solution_mapper.hpp index 127bdb2c96..58d37f7300 100644 --- a/cpp/src/grpc/grpc_solution_mapper.hpp +++ b/cpp/src/grpc/grpc_solution_mapper.hpp @@ -58,34 +58,6 @@ template cpu_mip_solution_t map_proto_to_mip_solution( const cuopt::remote::MIPSolution& pb_solution); -/** - * @brief Convert cuOpt termination status to protobuf enum. - * @param status cuOpt PDLP termination status - * @return Protobuf PDLPTerminationStatus enum - */ -cuopt::remote::PDLPTerminationStatus to_proto_pdlp_status(pdlp_termination_status_t status); - -/** - * @brief Convert protobuf enum to cuOpt termination status. - * @param status Protobuf PDLPTerminationStatus enum - * @return cuOpt PDLP termination status - */ -pdlp_termination_status_t from_proto_pdlp_status(cuopt::remote::PDLPTerminationStatus status); - -/** - * @brief Convert cuOpt MIP termination status to protobuf enum. - * @param status cuOpt MIP termination status - * @return Protobuf MIPTerminationStatus enum - */ -cuopt::remote::MIPTerminationStatus to_proto_mip_status(mip_termination_status_t status); - -/** - * @brief Convert protobuf enum to cuOpt MIP termination status. - * @param status Protobuf MIPTerminationStatus enum - * @return cuOpt MIP termination status - */ -mip_termination_status_t from_proto_mip_status(cuopt::remote::MIPTerminationStatus status); - // ============================================================================ // Chunked result support (for results exceeding gRPC max message size) // ============================================================================ diff --git a/cpp/src/grpc/server/grpc_field_element_size.hpp b/cpp/src/grpc/server/grpc_field_element_size.hpp index dd75d9c66c..5297853792 100644 --- a/cpp/src/grpc/server/grpc_field_element_size.hpp +++ b/cpp/src/grpc/server/grpc_field_element_size.hpp @@ -3,10 +3,6 @@ * reserved. SPDX-License-Identifier: Apache-2.0 */ -// Codegen target: this file maps ArrayFieldId enum values to their element byte sizes. -// A future version of cpp/codegen/generate_conversions.py can produce this from -// a problem_arrays section in field_registry.yaml. - #pragma once #ifdef CUOPT_ENABLE_GRPC @@ -16,27 +12,7 @@ inline int64_t array_field_element_size(cuopt::remote::ArrayFieldId field_id) { - switch (field_id) { - case cuopt::remote::FIELD_A_VALUES: - case cuopt::remote::FIELD_C: - case cuopt::remote::FIELD_B: - case cuopt::remote::FIELD_VARIABLE_LOWER_BOUNDS: - case cuopt::remote::FIELD_VARIABLE_UPPER_BOUNDS: - case cuopt::remote::FIELD_CONSTRAINT_LOWER_BOUNDS: - case cuopt::remote::FIELD_CONSTRAINT_UPPER_BOUNDS: - case cuopt::remote::FIELD_Q_VALUES: - case cuopt::remote::FIELD_INITIAL_PRIMAL_SOLUTION: - case cuopt::remote::FIELD_INITIAL_DUAL_SOLUTION: return 8; - case cuopt::remote::FIELD_A_INDICES: - case cuopt::remote::FIELD_A_OFFSETS: - case cuopt::remote::FIELD_Q_INDICES: - case cuopt::remote::FIELD_Q_OFFSETS: - case cuopt::remote::FIELD_VARIABLE_TYPES: return 4; - case cuopt::remote::FIELD_ROW_TYPES: - case cuopt::remote::FIELD_VARIABLE_NAMES: - case cuopt::remote::FIELD_ROW_NAMES: return 1; - } - return -1; +#include "generated_array_field_element_size.inc" } #endif // CUOPT_ENABLE_GRPC diff --git a/cpp/src/grpc/server/grpc_incumbent_proto.hpp b/cpp/src/grpc/server/grpc_incumbent_proto.hpp index f5c6f2e79e..e2f12d587a 100644 --- a/cpp/src/grpc/server/grpc_incumbent_proto.hpp +++ b/cpp/src/grpc/server/grpc_incumbent_proto.hpp @@ -4,7 +4,7 @@ */ // Codegen target: this file builds and parses cuopt::remote::Incumbent protobuf messages. -// A future version of cpp/codegen/generate_conversions.py can produce this from +// A future version of cpp/src/grpc/codegen/generate_conversions.py can produce this from // an incumbent section in field_registry.yaml. #pragma once diff --git a/cpp/tests/linear_programming/grpc/CMakeLists.txt b/cpp/tests/linear_programming/grpc/CMakeLists.txt index 74f15f5cfa..aadbf23df3 100644 --- a/cpp/tests/linear_programming/grpc/CMakeLists.txt +++ b/cpp/tests/linear_programming/grpc/CMakeLists.txt @@ -16,6 +16,7 @@ target_include_directories(GRPC_CLIENT_TEST "${CUOPT_SOURCE_DIR}/include" "${CUOPT_SOURCE_DIR}/src/grpc" "${CUOPT_SOURCE_DIR}/src/grpc/client" + "${CUOPT_SOURCE_DIR}/src/grpc/codegen/generated" "${CUOPT_TEST_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" # For grpc_client_test_helper.hpp "${CMAKE_BINARY_DIR}" # For generated protobuf headers @@ -60,6 +61,7 @@ target_include_directories(GRPC_PIPE_SERIALIZATION_TEST "${CUOPT_SOURCE_DIR}/include" "${CUOPT_SOURCE_DIR}/src/grpc" "${CUOPT_SOURCE_DIR}/src/grpc/server" + "${CUOPT_SOURCE_DIR}/src/grpc/codegen/generated" "${CMAKE_BINARY_DIR}" # For generated protobuf headers ) diff --git a/cpp/tests/linear_programming/grpc/grpc_pipe_serialization_test.cpp b/cpp/tests/linear_programming/grpc/grpc_pipe_serialization_test.cpp index 632f9ce8d2..d84574680d 100644 --- a/cpp/tests/linear_programming/grpc/grpc_pipe_serialization_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_pipe_serialization_test.cpp @@ -294,7 +294,7 @@ TEST(PipeSerialization, Result_RoundTrip) PipePair pp; ChunkedResultHeader header; - header.set_problem_category(cuopt::remote::LP); + header.set_problem_category(LP); header.set_lp_termination_status(PDLP_OPTIMAL); header.set_primal_objective(42.5); header.set_solve_time(1.23); @@ -319,7 +319,7 @@ TEST(PipeSerialization, Result_RoundTrip) ASSERT_TRUE(write_ok); ASSERT_TRUE(read_ok); - EXPECT_EQ(header_out.problem_category(), cuopt::remote::LP); + EXPECT_EQ(header_out.problem_category(), LP); EXPECT_EQ(header_out.lp_termination_status(), PDLP_OPTIMAL); EXPECT_DOUBLE_EQ(header_out.primal_objective(), 42.5); EXPECT_DOUBLE_EQ(header_out.solve_time(), 1.23); @@ -334,11 +334,11 @@ TEST(PipeSerialization, Result_MIPFields) PipePair pp; ChunkedResultHeader header; - header.set_problem_category(cuopt::remote::MIP); + header.set_problem_category(MIP); header.set_mip_termination_status(MIP_OPTIMAL); header.set_mip_objective(99.0); header.set_mip_gap(0.001); - header.set_mip_error_message(""); + header.set_error_message(""); auto solution = make_pattern(2000 * 8, 0x33); std::map> arrays; @@ -356,7 +356,7 @@ TEST(PipeSerialization, Result_MIPFields) ASSERT_TRUE(write_ok); ASSERT_TRUE(read_ok); - EXPECT_EQ(header_out.problem_category(), cuopt::remote::MIP); + EXPECT_EQ(header_out.problem_category(), MIP); EXPECT_EQ(header_out.mip_termination_status(), MIP_OPTIMAL); EXPECT_DOUBLE_EQ(header_out.mip_objective(), 99.0); @@ -369,7 +369,7 @@ TEST(PipeSerialization, Result_EmptyArrays) PipePair pp; ChunkedResultHeader header; - header.set_problem_category(cuopt::remote::LP); + header.set_problem_category(LP); header.set_error_message("solver failed"); std::map> arrays; // no arrays (error case) @@ -398,7 +398,7 @@ TEST(PipeSerialization, ProtobufRoundTrip) PipePair pp; ChunkedResultHeader msg; - msg.set_problem_category(cuopt::remote::MIP); + msg.set_problem_category(MIP); msg.set_primal_objective(3.14); msg.set_error_message("hello"); @@ -412,7 +412,7 @@ TEST(PipeSerialization, ProtobufRoundTrip) ASSERT_TRUE(write_ok); ASSERT_TRUE(read_ok); - EXPECT_EQ(msg_out.problem_category(), cuopt::remote::MIP); + EXPECT_EQ(msg_out.problem_category(), MIP); EXPECT_DOUBLE_EQ(msg_out.primal_objective(), 3.14); EXPECT_EQ(msg_out.error_message(), "hello"); } @@ -426,7 +426,7 @@ TEST(PipeSerialization, Result_LargeArray) PipePair pp; ChunkedResultHeader header; - header.set_problem_category(cuopt::remote::LP); + header.set_problem_category(LP); header.set_primal_objective(0.0); // ~4 MiB array — large enough to require many kernel-level pipe iterations. diff --git a/dependencies.yaml b/dependencies.yaml index 6076e1eae7..0415e89c76 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -299,6 +299,7 @@ dependencies: packages: - libboost-devel - cpp-argparse + - pyyaml - tbb-devel - zlib - bzip2 @@ -313,6 +314,7 @@ dependencies: - output_types: [conda] packages: - cuda-sanitizer-api + - pyyaml test_cpp_cuopt: common: - output_types: [conda] @@ -766,6 +768,7 @@ dependencies: - output_types: [conda, requirements] packages: - pre-commit + - ruamel.yaml>=0.18 - output_types: conda packages: - clang==20.1.8 diff --git a/docs/cuopt/grpc/GRPC_CODE_GENERATION.md b/docs/cuopt/grpc/GRPC_CODE_GENERATION.md new file mode 100644 index 0000000000..8d5e64b4ac --- /dev/null +++ b/docs/cuopt/grpc/GRPC_CODE_GENERATION.md @@ -0,0 +1,712 @@ +# Code Generation for gRPC Proto Definitions and C++ Conversion Code + +The code generator reads `field_registry.yaml` and produces `cuopt_remote_data.proto` +and C++ `.inc` files that are `#include`d directly into mapper source files. This +eliminates the need to hand-write repetitive conversion code or `.proto` definitions +— adding or removing a field is a one-line YAML change. + +## Quick Start + +```bash +# Regenerate after editing field_registry.yaml +python cpp/src/grpc/codegen/generate_conversions.py + +# Or with explicit paths: +python cpp/src/grpc/codegen/generate_conversions.py \ + --registry cpp/src/grpc/codegen/field_registry.yaml \ + --output-dir cpp/src/grpc/codegen/generated +``` + +The generator runs in ~100ms with no external dependencies beyond PyYAML (ships +with conda). The `--auto-number` and `--strip` options additionally require +`ruamel.yaml` (listed in the project's development dependencies). Run it +explicitly via `./build.sh codegen` after editing `field_registry.yaml`, then +commit the regenerated files. + +## File Layout + +``` +cpp/src/grpc/codegen/ +├── field_registry.yaml # Source of truth for all fields +├── generate_conversions.py # Generator script +└── generated/ # Output (committed, regenerated on build) + ├── cuopt_remote_data.proto + ├── generated_result_enums.proto.inc + │ + │ # Enum converters (one per domain) + ├── generated_enum_converters_problem.inc + ├── generated_enum_converters_settings.inc + ├── generated_enum_converters_solution.inc + │ + │ # Settings conversions + ├── generated_pdlp_settings_to_proto.inc + ├── generated_proto_to_pdlp_settings.inc + ├── generated_mip_settings_to_proto.inc + ├── generated_proto_to_mip_settings.inc + │ + │ # LP solution conversions + ├── generated_lp_solution_to_proto.inc + ├── generated_proto_to_lp_solution.inc + ├── generated_lp_chunked_header.inc + ├── generated_collect_lp_arrays.inc + ├── generated_chunked_to_lp_solution.inc + ├── generated_estimate_lp_size.inc + │ + │ # MIP solution conversions + ├── generated_mip_solution_to_proto.inc + ├── generated_proto_to_mip_solution.inc + ├── generated_mip_chunked_header.inc + ├── generated_collect_mip_arrays.inc + ├── generated_chunked_to_mip_solution.inc + ├── generated_estimate_mip_size.inc + │ + │ # Problem conversions + ├── generated_problem_to_proto.inc + ├── generated_proto_to_problem.inc + ├── generated_estimate_problem_size.inc + ├── generated_populate_chunked_header_lp.inc + ├── generated_populate_chunked_header_mip.inc + ├── generated_chunked_header_to_problem.inc + ├── generated_chunked_arrays_to_problem.inc + ├── generated_build_array_chunks.inc + └── generated_array_field_element_size.inc +``` + +The generated `.inc` files are committed to the repo so that builds work without +running the generator. Run `./build.sh codegen` to re-generate them after +editing `field_registry.yaml`; CI verifies they stay in sync. CMake adds +`cpp/src/grpc/codegen/generated` to the include path for both targets, so the `.inc` +files are found at compile time with no copy step. + +--- + +## Registry Structure Overview + +`field_registry.yaml` has these top-level sections: + +| Section | Purpose | +|---|---| +| `enums` | Shared enum definitions (C++ ↔ proto converters) | +| `lp_solution` | LP solution scalar/array fields and constructor args | +| `mip_solution` | MIP solution scalar/array fields and constructor args | +| `pdlp_settings` | PDLP solver settings field mappings | +| `mip_settings` | MIP solver settings field mappings | +| `optimization_problem` | Problem input scalar/array fields and setter groups | + +--- + +## Convention-Over-Configuration Defaults + +The registry uses bare field names wherever possible. Defaults: + +| Context | Default type | Default getter | Default setter | +|---|---|---|---| +| Solution scalar | `double` | `get_()` | *(via constructor)* | +| Solution array | `double` | `get__host()` | *(via constructor)* | +| Problem scalar | `double` | `get_()` | `set_()` | +| Problem array | `repeated double` | `get__host()` | `set_()` | +| Settings field | `double` | struct member access | struct member assignment | + +Proto field names always match the registry field name. + +**Enum conventions** (derived from the YAML key unless overridden): + +| Property | Convention | +|---|---| +| `cpp_type` | `_t` (e.g. `pdlp_termination_status` → `pdlp_termination_status_t`) | +| `proto_type` | PascalCase from key, with known acronyms uppercased: PDLP, MIP, LP, QP, VRP, PDP, TSP (e.g. `pdlp_termination_status` → `PDLPTerminationStatus`) | +| `default` | First value in the `values` list (the proto3 zero-value) | +| `values` numbering | 0, 1, 2, ... (bare names auto-number; `{Name: N}` resets counter to N) | +| converter fns | `to_proto_()` / `from_proto_()` | + +--- + +## Enums + +Each entry under `enums:` defines a C++ ↔ proto enum mapping. The generator +produces `to_proto_()` and `from_proto_()` switch functions, split +into per-domain `.inc` files (`generated_enum_converters_problem.inc`, etc.). + +Most properties are derived by convention (see above). Only `domain` and +`values` are required for the common case. Values auto-number from 0 when +written as bare names: + +```yaml +enums: + # Minimal — bare names auto-number 0, 1, 2, ... + # proto_type, cpp_type, and default are all derived. + pdlp_termination_status: + domain: solution # groups into generated_enum_converters_solution.inc + proto_prefix: PDLP # proto value = PDLP_UPPER_SNAKE(CppName) + values: + - NoTermination # = 0 + - NumericalError # = 1 + - Optimal # = 2 + - PrimalInfeasible # = 3 + + # Override default when it's not the first value + pdlp_solver_mode: + domain: settings + default: Stable3 + values: + - Stable1 + - Stable2 + - Methodical1 + - Fast1 + - Stable3 + + # Override cpp_type when it doesn't follow _t + lp_method: + domain: settings + cpp_type: method_t + values: + - Concurrent + - PDLP + - DualSimplex + - Barrier + + # Explicit values reset the counter (C-style enum semantics): + # example_with_gaps: + # domain: solution + # values: + # - OK # = 0 + # - Warning: 10 # explicit → resets counter + # - Error # = 11 (continues from 10+1) + # - Fatal: 20 # explicit → resets counter + # - Panic # = 21 +``` + +### Enum properties + +| Property | Required | Default | Description | +|---|---|---|---| +| `domain` | yes | — | One of `problem`, `settings`, `solution`. Controls which `.inc` file the converters go into | +| `proto_type` | no | PascalCase from key | Protobuf enum type name. Derived via acronym-aware PascalCase (e.g. `pdlp_termination_status` → `PDLPTerminationStatus`). Override when the derived name doesn't match | +| `proto_prefix` | no | *(none)* | Prefix for proto value names. With prefix `PDLP`, C++ `Optimal` becomes proto `PDLP_OPTIMAL` | +| `cpp_type` | no | `_t` | C++ enum type (e.g. `pdlp_termination_status_t`). Override with `cpp_type: method_t` | +| `default` | no | first value | C++ enum value to return for unrecognized proto values. Defaults to the first entry in `values` (the proto3 zero-value). Override when the default differs (e.g. `pdlp_solver_mode` defaults to `Stable3`) | +| `values` | yes | — | List of enum entries. Bare names auto-number from 0. Use `{Name: N}` to override; subsequent bare names continue from N+1 (C-style enum semantics) | + +--- + +## LP / MIP Solution Sections + +Each solution section (`lp_solution`, `mip_solution`) generates six `.inc` files +covering unary proto conversion, chunked streaming, size estimation, and array +collection. + +```yaml +lp_solution: + cpp_type: "cpu_lp_solution_t" + + scalars: [...] + arrays: [...] + constructor_args: { scalars: [...] } + warm_start: { ... } # LP only +``` + +### Top-level properties + +| Property | Description | +|---|---| +| `cpp_type` | Fully-qualified C++ template type for the solution constructor | + +The generator derives `ChunkedResultHeader.problem_category` (LP or MIP) +automatically from the section name (`lp_solution` vs `mip_solution`). + +### Scalars + +Each entry describes one scalar field on both `ChunkedResultHeader` (for +chunked streaming) and the unary solution proto. + +```yaml +scalars: + # Minimal form — double type, getter = get_gap() + - gap: + field_num: 1006 + + # Enum type + - lp_termination_status: + field_num: 1000 + type: pdlp_termination_status # references an enum key + getter: get_termination_status() + + # Proto-only (set on header but NOT a constructor arg) + - error_message: + field_num: 1001 + type: string + getter: "get_error_status().what()" + proto_only: true +``` + +| Property | Default | Description | +|---|---|---| +| `field_num` | *(required)* | Proto field number in `ChunkedResultHeader`. LP: 1000–1999, MIP: 2000–2999, warm start: 3000–3999 | +| `type` | `double` | One of: `double`, `int32`, `bool`, `string`, or an enum key name | +| `getter` | `get_()` | C++ expression to read from the solution object | +| `proto_only` | `false` | If true, set on the proto header but not passed to the C++ constructor | + +#### Type-specific behavior + +| `type` | To-proto cast | From-proto cast | +|---|---|---| +| `double` | `static_cast(...)` | `static_cast(...)` | +| `int32` | `static_cast(...)` | `static_cast(...)` | +| `bool` | *(none)* | *(none)* | +| `string` | *(none)* | *(none)* | +| enum key | `to_proto_(...)` | `from_proto_(...)` | + +### Arrays + +Each entry describes a solution array, identified by a `ResultFieldId` enum value. + +```yaml +arrays: + - primal_solution: + field_num: 1 + array_id: 0 + + - mip_solution: + array_id: 12 + field_num: 1 + getter: get_solution_host() # override the default +``` + +| Property | Default | Description | +|---|---|---| +| `field_num` | *(required)* | Proto field number in the per-solution unary message | +| `array_id` | *(required)* | Numeric value for the `ResultFieldId` enum (global across LP + MIP) | +| `getter` | `get__host()` | C++ getter expression on the solution object | + +### Constructor Args + +Controls the positional argument order when reconstructing a C++ solution +object from proto data: + +```yaml +constructor_args: + scalars: + - lp_termination_status + - primal_objective + - dual_objective + # ... order must match the C++ constructor +``` + +Arrays are always passed first (in YAML declaration order) via `std::move`. +Then the scalars listed here. If a `warm_start` section exists and warm start +data is present, `std::move(ws)` is appended as the final argument. + +### Warm Start (LP only) + +Describes a conditional sub-object for PDLP warm start data: + +```yaml +warm_start: + presence_check: has_warm_start_data() # predicate on the solution object + getter: get_cpu_pdlp_warm_start_data() # accessor for the WS struct + + scalars: + - initial_primal_weight_: + field_num: 3000 + + arrays: + - current_primal_solution_: + field_num: 1 + array_id: 3 +``` + +| Property | Description | +|---|---| +| `presence_check` | C++ predicate expression to test if warm start data is present on the solution object | +| `getter` | C++ expression to access the warm start struct | + +Warm start field names match the C++ struct member names directly (e.g. +`initial_primal_weight_` maps to `ws.initial_primal_weight_`). The `member` +attribute is only needed if the proto field name cannot match the C++ name +due to ambiguity. + +Warm start detection during chunked deserialization is auto-derived: if the +first array in the warm start section is present (non-empty), warm start data +is considered present. + +--- + +## Settings Sections + +Each settings section (`pdlp_settings`, `mip_settings`) generates two `.inc` +files: `generated_{label}_settings_to_proto.inc` and +`generated_proto_to_{label}_settings.inc`. + +```yaml +pdlp_settings: + cpp_type: "pdlp_solver_settings_t" + proto_type: "cuopt::remote::PDLPSolverSettings" + + fields: + # Nested sub-struct — generates settings.tolerances. + - tolerances: + - absolute_gap_tolerance: + field_num: 1 + - relative_gap_tolerance: + field_num: 2 + + # Top-level fields + - time_limit: + field_num: 9 + - iteration_limit: + field_num: 10 + type: int64 + sentinel: + to_proto: "std::numeric_limits::max()" + proto_value: -1 + from_proto_guard: ">= 0" + from_proto_cast: "i_t" +``` + +Settings fields support **nesting**: a list-valued entry (like `tolerances` +above) represents a sub-struct. The generator automatically prefixes C++ member +access with the sub-struct path (e.g. `settings.tolerances.absolute_gap_tolerance`). + +### Settings field properties + +| Property | Default | Description | +|---|---|---| +| `field_num` | *(required)* | Proto field number in the settings message | +| `type` | `double` | One of: `double`, `int32`, `int64`, `bool`, `string`, or an enum key | +| `cpp_member` | `` (auto-prefixed by nesting path) | Explicit path to the C++ struct member | +| `to_proto_cast` | *(none)* | Explicit cast for C++ → proto (e.g. `int32_t`) | +| `from_proto_cast` | *(none)* | Explicit cast for proto → C++ (e.g. `presolver_t`) | +| `sentinel` | *(none)* | Special handling for sentinel values (see below) | + +### Sentinel values + +Some fields map a C++ default (like `max()`) to a proto sentinel (like `-1`): + +```yaml +- iteration_limit: + type: int64 + sentinel: + to_proto: "std::numeric_limits::max()" # if C++ value == this... + proto_value: -1 # ...emit this in proto + from_proto_guard: ">= 0" # only assign if proto value matches guard + from_proto_cast: "i_t" # cast applied when assigning +``` + +--- + +## Optimization Problem Section + +The `optimization_problem` section generates the most files — problem +serialization/deserialization for both unary and chunked gRPC paths. + +```yaml +optimization_problem: + cpp_type: "cpu_optimization_problem_t" + proto_message: OptimizationProblem + + scalars: [...] + arrays: [...] + setter_groups: { ... } +``` + +### Scalars + +```yaml +scalars: +- problem_name: + field_num: 1 + type: string +- maximize: + field_num: 3 + type: bool + getter: get_sense() +- problem_category: + field_num: 6 + type: problem_category # enum key reference +``` + +| Property | Default | Description | +|---|---|---| +| `field_num` | *(required)* | Proto field number in `OptimizationProblem` | +| `type` | `double` | One of: `double`, `int32`, `bool`, `string`, or an enum key | +| `getter` | `get_()` | C++ getter on `cpu_optimization_problem_t` | +| `setter` | `set_()` | C++ setter (can be overridden via `setter_getter_root`) | +| `setter_getter_root` | `` | Base name for default getter/setter derivation | + +### Arrays + +```yaml +arrays: +- variable_names: + array_id: 0 + field_num: 7 + type: repeated string + +- A_values: + array_id: 2 + field_num: 9 + setter_getter_root: constraint_matrix_values + setter_group: csr_constraint_matrix + +- variable_types: + array_id: 12 + field_num: 19 + type: repeated variable_type # repeated enum + +- row_types: + array_id: 11 + field_num: 18 + type: bytes + conditional: true +``` + +| Property | Default | Description | +|---|---|---| +| `field_num` | *(required)* | Proto field number in `OptimizationProblem` | +| `array_id` | *(required)* | Numeric value for the `ArrayFieldId` enum | +| `type` | `repeated double` | One of: `repeated double`, `repeated int32`, `repeated string`, `bytes`, or `repeated ` | +| `getter` | `get__host()` | C++ getter (strings use `get_()` without `_host`) | +| `setter` | `set_()` | C++ setter | +| `setter_getter_root` | `` | Base name for getter/setter derivation when different from field name | +| `setter_group` | *(none)* | Name of a multi-argument setter group (see below) | +| `conditional` | `false` | If true, serialization is guarded by an emptiness check | +| `skip_conversion` | `false` | If true, the field appears in the proto but is excluded from conversion code | + +### Setter Groups + +Some C++ setters take multiple arrays at once (e.g. CSR matrix = values + +indices + offsets). Setter groups handle this: + +```yaml +setter_groups: + csr_constraint_matrix: + setter: set_csr_constraint_matrix + fields: [A_values, A_indices, A_offsets] + + quadratic_objective: + setter: set_quadratic_objective_matrix + fields: [Q_values, Q_indices, Q_offsets] +``` + +| Property | Description | +|---|---| +| `setter` | C++ setter function name (called with all field arrays as arguments) | +| `fields` | Ordered list of array field names that are passed to the setter | + +Arrays that belong to a setter group are excluded from normal per-field +deserialization and handled as a batch instead. + +During deserialization, the generator automatically guards setter group calls +by checking if the first field has data (e.g. `if (pb_problem.a_values_size() > 0)`). +This is derived from the group structure — no explicit condition attribute is needed. + +--- + +## Field Number Allocation + +Field numbers are required for proto compatibility. They can be assigned +manually or auto-assigned. + +### Manual assignment + +Specify `field_num` and `array_id` on each field entry. This is the default +workflow. + +### Auto-assignment + +Run with `--auto-number` to fill in any missing `field_num` or `array_id` +values. This requires `ruamel.yaml` (preserves YAML comments and formatting): + +```bash +python cpp/src/grpc/codegen/generate_conversions.py --auto-number +``` + +### Stripping field numbers + +Run with `--strip` to remove all `field_num` and `array_id` values from +`field_registry.yaml`. This is useful for reviewing pure field definitions +without numbering clutter, or for forcing a full re-assignment via +`--auto-number`: + +```bash +python cpp/src/grpc/codegen/generate_conversions.py --strip +python cpp/src/grpc/codegen/generate_conversions.py --auto-number +``` + +If all field numbers have been stripped, running the generator without +`--auto-number` will produce an error. + +The numbering ranges: + +| Scope | Range | +|---|---| +| `optimization_problem` field_num | 1+ (contiguous, shared across scalars and arrays) | +| `optimization_problem` array_id | 0+ (separate namespace) | +| LP solution scalars (ChunkedResultHeader) | 1000–1999 | +| MIP solution scalars (ChunkedResultHeader) | 2000–2999 | +| Warm start scalars (ChunkedResultHeader) | 3000–3999 | +| Solution array array_id | 0+ (global pool shared across LP, MIP, and warm start) | +| Solution array field_num | 1+ per message (no cap) | +| Settings field_num | 1+ per message (no cap) | + +--- + +## What Gets Generated + +### `cuopt_remote_data.proto` + +A complete proto file with all data messages and enums derived from the +registry: `OptimizationProblem`, `PDLPSolverSettings`, `MIPSolverSettings`, +`PDLPWarmStartData`, `LPSolution`, `MIPSolution`, `ChunkedResultHeader`, +`ResultArrayDescriptor`, and the enums they reference (`PDLPTerminationStatus`, +`MIPTerminationStatus`, `PDLPSolverMode`, `LPMethod`, `VariableType`, +`ProblemCategory`, `ResultFieldId`, `ArrayFieldId`). + +The hand-maintained `cuopt_remote.proto` and `cuopt_remote_service.proto` can +import this generated file to avoid duplicating definitions. + +### Enum converter `.inc` files + +Per-domain C++ switch functions, split by the `domain` tag on each enum: + +- `generated_enum_converters_problem.inc` — enums with `domain: problem` +- `generated_enum_converters_settings.inc` — enums with `domain: settings` +- `generated_enum_converters_solution.inc` — enums with `domain: solution` + +### Settings `.inc` files + +- `generated_pdlp_settings_to_proto.inc` / `generated_proto_to_pdlp_settings.inc` +- `generated_mip_settings_to_proto.inc` / `generated_proto_to_mip_settings.inc` + +### Solution `.inc` files (6 per solution type) + +For each of LP and MIP: + +| File | Function body it provides | +|---|---| +| `generated_{lp,mip}_solution_to_proto.inc` | Unary C++ solution → proto | +| `generated_proto_to_{lp,mip}_solution.inc` | Unary proto → C++ solution | +| `generated_{lp,mip}_chunked_header.inc` | Populate `ChunkedResultHeader` | +| `generated_collect_{lp,mip}_arrays.inc` | Collect solution arrays as byte maps | +| `generated_chunked_to_{lp,mip}_solution.inc` | Reassemble C++ solution from chunked data | +| `generated_estimate_{lp,mip}_size.inc` | Estimate serialized proto size | + +### Problem `.inc` files + +| File | Function body it provides | +|---|---| +| `generated_problem_to_proto.inc` | C++ problem → unary proto | +| `generated_proto_to_problem.inc` | Unary proto → C++ problem | +| `generated_estimate_problem_size.inc` | Estimate serialized problem size | +| `generated_populate_chunked_header_{lp,mip}.inc` | Populate chunked problem header | +| `generated_chunked_header_to_problem.inc` | Set problem scalars from chunked header | +| `generated_chunked_arrays_to_problem.inc` | Set problem arrays from chunked byte maps | +| `generated_build_array_chunks.inc` | Build `SendArrayChunkRequest` list for upload | +| `generated_array_field_element_size.inc` | Switch body for per-field element byte size | + +--- + +## How `.inc` Files Are Consumed + +The `.inc` files are `#include`d directly inside C++ function bodies in: + +- `cpp/src/grpc/grpc_settings_mapper.cpp` +- `cpp/src/grpc/grpc_solution_mapper.cpp` +- `cpp/src/grpc/grpc_problem_mapper.cpp` +- `cpp/src/grpc/server/grpc_field_element_size.hpp` + +CMake adds `cpp/src/grpc/codegen/generated` to the include path for the `cuopt` and +`cuopt_grpc_server` targets, so the bare `#include "generated_*.inc"` directives +resolve without any copy step. + +--- + +## Adding a New Field — Walkthroughs + +### Add `dual_bound` (double) to MIP solution + +1. Add to `mip_solution.scalars`: + ```yaml + - dual_bound: + field_num: 2012 + ``` + +2. Add to `mip_solution.constructor_args.scalars` in the correct position: + ```yaml + constructor_args: + scalars: + - mip_termination_status + - mip_objective + # ... + - dual_bound # ← new, position must match C++ constructor + ``` + +3. Add the constructor parameter to `cpu_mip_solution_t`. + +4. Regenerate: + ```bash + python cpp/src/grpc/codegen/generate_conversions.py + ``` + +5. Build and test. + +The proto field in `ChunkedResultHeader` and the solution message are generated +automatically — no manual `.proto` edits needed. + +### Add `detect_infeasibility_v2` (bool) to PDLP settings + +1. Add to `pdlp_settings.fields`: + ```yaml + - detect_infeasibility_v2: + field_num: 31 + type: bool + ``` + +2. Add the C++ struct member to `pdlp_solver_settings_t`: + ```cpp + bool detect_infeasibility_v2{false}; + ``` + +3. Regenerate and build. You never touch `grpc_settings_mapper.cpp` or any + `.proto` file — the proto field in `PDLPSolverSettings` is generated + automatically. + +### Add a new array to the optimization problem + +1. Add to `optimization_problem.arrays`: + ```yaml + - my_new_array: + array_id: 18 + field_num: 25 + type: repeated double + ``` + +2. Add getter/setter to `cpu_optimization_problem_t`. + +3. Regenerate and build. + +The `ArrayFieldId` enum entry and the `OptimizationProblem` proto field are +generated automatically. + +### Add a tolerance to MIP settings (nested sub-struct) + +If the C++ member is nested under `tolerances.`, just add it inside the +`tolerances` list: + +```yaml +- tolerances: + - relative_mip_gap: + field_num: 2 + - my_new_tolerance: # ← new + field_num: 14 +``` + +The generator will access it as `settings.tolerances.my_new_tolerance`. + +--- + +## Related Documentation + +- `GRPC_INTERFACE.md` — Chunked transfer protocol, message size limits, error handling. +- `GRPC_SERVER_ARCHITECTURE.md` — Server process model, IPC, threads, job lifecycle. +- `GRPC_QUICK_START.md` — Starting the server and solving remotely from Python, CLI, or C. diff --git a/docs/cuopt/grpc/GRPC_INTERFACE.md b/docs/cuopt/grpc/GRPC_INTERFACE.md new file mode 100644 index 0000000000..e85498ef15 --- /dev/null +++ b/docs/cuopt/grpc/GRPC_INTERFACE.md @@ -0,0 +1,133 @@ +# gRPC Chunked Transfer Protocol + +## Overview + +The cuOpt remote execution system uses gRPC for client-server communication. The interface +supports arbitrarily large optimization problems (multi-GB) through a chunked array transfer +protocol that uses only unary (request-response) RPCs — no bidirectional streaming. + +## Chunked Array Transfer Protocol + +### Why Chunking? + +gRPC has per-message size limits (configurable, default set to 256 MiB in cuOpt), and +protobuf has a hard 2 GB serialization limit. Optimization problems and their solutions +can exceed several gigabytes, so a chunked transfer mechanism is needed. + +The protocol uses only **unary RPCs** (no bidirectional streaming), which simplifies +error handling, load balancing, and proxy compatibility. + +### Upload Protocol (Large Problems) + +When the estimated serialized problem size exceeds 75% of `max_message_bytes`, the client +splits large arrays into chunks and sends them via multiple unary RPCs: + +```text +Client Server + | | + |-- StartChunkedUpload(header, settings) -----> | + |<-- upload_id, max_message_bytes -------------- | + | | + |-- SendArrayChunk(upload_id, field, data) ----> | + |<-- ok ---------------------------------------- | + | | + |-- SendArrayChunk(upload_id, field, data) ----> | + |<-- ok ---------------------------------------- | + | ... | + | | + |-- FinishChunkedUpload(upload_id) ------------> | + |<-- job_id ------------------------------------ | +``` + +**Key features:** +- `StartChunkedUpload` sends a `ChunkedProblemHeader` with all scalar fields + and solver settings (no per-array metadata in the header) +- Each `SendArrayChunk` carries one chunk of one array via an `ArrayChunk` + message, which includes the `ArrayFieldId`, `element_offset`, and + `total_elements` (for server-side pre-allocation) +- The server reports `max_message_bytes` so the client can adapt chunk sizing +- `FinishChunkedUpload` triggers server-side reassembly and job submission + +### Download Protocol (Large Results) + +When the result exceeds the gRPC max message size, the client fetches it via +chunked unary RPCs (mirrors the upload pattern): + +```text +Client Server + | | + |-- StartChunkedDownload(job_id) --------------> | + |<-- download_id, ChunkedResultHeader ---------- | + | | + |-- GetResultChunk(download_id, field, off) ----> | + |<-- data bytes --------------------------------- | + | | + |-- GetResultChunk(download_id, field, off) ----> | + |<-- data bytes --------------------------------- | + | ... | + | | + |-- FinishChunkedDownload(download_id) ---------> | + |<-- ok ----------------------------------------- | +``` + +**Key features:** +- `ChunkedResultHeader` carries all scalar fields (termination status, objectives, + residuals, solve time, warm start scalars) plus `ResultArrayDescriptor` entries + for each array (solution vectors, warm start arrays) +- Each `GetResultChunk` fetches a slice of one array, identified by `ResultFieldId` + and `element_offset` +- `FinishChunkedDownload` releases the server-side download session state +- LP results include PDLP warm start data (9 arrays + 8 scalars) for subsequent + warm-started solves + +### Automatic Routing + +The client handles size-based routing transparently: + +1. **Upload**: Estimate serialized problem size + - Below 75% of `max_message_bytes` → unary `SubmitJob` + - Above threshold → `StartChunkedUpload` + `SendArrayChunk` + `FinishChunkedUpload` +2. **Download**: Check `result_size_bytes` from `CheckStatus` + - Below `max_message_bytes` → unary `GetResult` + - Above limit (or `RESOURCE_EXHAUSTED`) → chunked download RPCs + +## Message Size Limits + +| Configuration | Default | Notes | +|---------------|---------|-------| +| Server `--max-message-mb` | 256 MiB | Per-message limit (also `--max-message-bytes` for exact byte values) | +| Server clamping | [4 KiB, ~2 GiB] | Enforced at startup to stay within protobuf's serialization limit | +| Client `max_message_bytes` | 256 MiB | Clamped to [4 MiB, ~2 GiB] at construction | +| Chunk size | 16 MiB | Payload per `SendArrayChunk`/`GetResultChunk` | +| Chunked threshold | 75% of max_message_bytes | Problems above this use chunked upload (e.g. 192 MiB when max is 256 MiB) | + +Chunked transfer allows unlimited total payload size; only individual +chunks must fit within the per-message limit. Neither client nor server +allows "unlimited" message size — both clamp to the protobuf 2 GiB ceiling. + +## Error Handling + +### gRPC Status Codes + +| Code | Meaning | Client Action | +|------|---------|---------------| +| `OK` | Success | Process result | +| `NOT_FOUND` | Job ID not found | Check job ID | +| `RESOURCE_EXHAUSTED` | Message too large | Use chunked transfer | +| `CANCELLED` | Job was cancelled | Handle gracefully | +| `DEADLINE_EXCEEDED` | Timeout | Retry or increase timeout | +| `UNAVAILABLE` | Server not reachable | Retry with backoff | +| `INTERNAL` | Server error | Report to user | +| `INVALID_ARGUMENT` | Bad request | Fix request | + +### Connection Handling + +- Client detects `context->IsCancelled()` for graceful disconnect +- Server cleans up job state on client disconnect during upload +- Automatic reconnection is NOT built-in (caller should retry) + +## Related Documentation + +- `GRPC_SERVER_ARCHITECTURE.md` — Server process model, IPC, threads, job lifecycle. +- `GRPC_QUICK_START.md` — Starting the server and solving remotely from Python, CLI, or C. +- `GRPC_CODE_GENERATION.md` — Registry format, generated file inventory, and walkthrough examples. From 4b172d0b75f98e6f975d00eca3de5ec345a5eb7b Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Thu, 16 Apr 2026 14:38:07 -0400 Subject: [PATCH 02/15] coderabbit feedback on grpc code generator --- ci/test_cpp.sh | 4 +- ci/verify_codegen.sh | 57 ------------------- .../all_cuda-129_arch-aarch64.yaml | 1 - .../all_cuda-129_arch-x86_64.yaml | 1 - .../all_cuda-131_arch-aarch64.yaml | 1 - .../all_cuda-131_arch-x86_64.yaml | 1 - conda/recipes/libcuopt/recipe.yaml | 2 - cpp/src/grpc/codegen/generate_conversions.py | 14 ++--- dependencies.yaml | 6 +- docs/cuopt/grpc/GRPC_CODE_GENERATION.md | 2 +- 10 files changed, 13 insertions(+), 76 deletions(-) delete mode 100755 ci/verify_codegen.sh diff --git a/ci/test_cpp.sh b/ci/test_cpp.sh index 29576b3dfb..20346d59d1 100755 --- a/ci/test_cpp.sh +++ b/ci/test_cpp.sh @@ -31,8 +31,8 @@ mkdir -p "${RAPIDS_TESTS_DIR}" rapids-print-env -rapids-logger "Verify codegen output matches committed files" -./ci/verify_codegen.sh +rapids-logger "Verify gRPC codegen output matches committed files" +./ci/verify_grpc_codegen.sh rapids-logger "Check GPU usage" nvidia-smi diff --git a/ci/verify_codegen.sh b/ci/verify_codegen.sh deleted file mode 100755 index c2ddf63bbf..0000000000 --- a/ci/verify_codegen.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Verify that committed codegen output matches what generate_conversions.py produces. -# Fails if a developer edited field_registry.yaml without re-running ./build.sh codegen. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -CODEGEN_DIR="${REPO_DIR}/cpp/src/grpc/codegen" -GENERATED_DIR="${CODEGEN_DIR}/generated" -PROTO_DEST="${REPO_DIR}/cpp/src/grpc/cuopt_remote_data.proto" - -TMPDIR=$(mktemp -d) -trap 'rm -rf ${TMPDIR}' EXIT - -echo "Running code generator into temp directory..." -python "${CODEGEN_DIR}/generate_conversions.py" \ - --registry "${CODEGEN_DIR}/field_registry.yaml" \ - --output-dir "${TMPDIR}" - -echo "Comparing generated output with committed files..." - -FAILED=0 - -for f in "${TMPDIR}"/*; do - fname=$(basename "$f") - committed="${GENERATED_DIR}/${fname}" - if [ ! -f "${committed}" ]; then - echo "MISSING: ${committed} (new generated file not committed)" - FAILED=1 - continue - fi - if ! diff -q "$f" "${committed}" > /dev/null 2>&1; then - echo "MISMATCH: cpp/src/grpc/codegen/generated/${fname}" - diff -u "${committed}" "$f" | head -30 - FAILED=1 - fi -done - -if [ -f "${TMPDIR}/cuopt_remote_data.proto" ] && [ -f "${PROTO_DEST}" ]; then - if ! diff -q "${TMPDIR}/cuopt_remote_data.proto" "${PROTO_DEST}" > /dev/null 2>&1; then - echo "MISMATCH: cpp/src/grpc/cuopt_remote_data.proto (not copied from codegen/generated)" - FAILED=1 - fi -fi - -if [ ${FAILED} -ne 0 ]; then - echo "" - echo "ERROR: Committed generated files are out of sync with field_registry.yaml." - echo "Run './build.sh codegen' and commit the results." - exit 1 -fi - -echo "OK: All generated files match field_registry.yaml." diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 7aee56978c..618f73b620 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -59,7 +59,6 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 -- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index 537100f10c..3f5b8e775b 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -59,7 +59,6 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 -- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 diff --git a/conda/environments/all_cuda-131_arch-aarch64.yaml b/conda/environments/all_cuda-131_arch-aarch64.yaml index 10009108b9..d876a3733e 100644 --- a/conda/environments/all_cuda-131_arch-aarch64.yaml +++ b/conda/environments/all_cuda-131_arch-aarch64.yaml @@ -59,7 +59,6 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 -- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 diff --git a/conda/environments/all_cuda-131_arch-x86_64.yaml b/conda/environments/all_cuda-131_arch-x86_64.yaml index 9c4b7308ad..457c27dc04 100644 --- a/conda/environments/all_cuda-131_arch-x86_64.yaml +++ b/conda/environments/all_cuda-131_arch-x86_64.yaml @@ -59,7 +59,6 @@ dependencies: - pytest-cov - pytest<9.0 - python>=3.11,<3.15 -- pyyaml - pyyaml>=6.0.0 - rapids-build-backend>=0.4.0,<0.5.0 - rapids-logger==0.2.*,>=0.0.0a0 diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 93447c1924..682f9d33ef 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -74,8 +74,6 @@ cache: - make - ninja - git - - python - - pyyaml - tbb-devel - zlib - bzip2 diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index a0401a4c1a..407b23742c 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -55,7 +55,7 @@ def parse_field(entry): return {"name": entry} if isinstance(entry, dict): assert len(entry) == 1, f"Expected single-key dict, got {entry}" - name = list(entry.keys())[0] + name = next(iter(entry)) val = entry[name] if isinstance(val, str): return {"name": name, "type": val} @@ -72,7 +72,7 @@ def parse_settings_fields(entries, prefix=""): """Parse settings fields, handling nested sub-structs (e.g. tolerances).""" for entry in entries: assert isinstance(entry, dict) and len(entry) == 1 - name = list(entry.keys())[0] + name = next(iter(entry)) val = entry[name] if isinstance(val, list): yield from parse_settings_fields(val, prefix=f"{prefix}{name}.") @@ -97,7 +97,7 @@ def parse_enum_entry(entry, index=0): if isinstance(entry, str): return entry, index assert isinstance(entry, dict) and len(entry) == 1 - name = list(entry.keys())[0] + name = next(iter(entry)) num = entry[name] return name, num if num is not None else index @@ -1923,7 +1923,7 @@ def _collect_field_nums(entries, key_name="field_num"): nums = set() for entry in entries: if isinstance(entry, dict) and len(entry) == 1: - name = list(entry.keys())[0] + name = next(iter(entry)) val = entry[name] if isinstance(val, dict) and key_name in val: nums.add(val[key_name]) @@ -1935,7 +1935,7 @@ def _collect_settings_field_nums(entries): nums = set() for entry in entries: if isinstance(entry, dict) and len(entry) == 1: - name = list(entry.keys())[0] + name = next(iter(entry)) val = entry[name] if isinstance(val, list): nums |= _collect_settings_field_nums(val) @@ -1971,7 +1971,7 @@ def _assign_to_field_list(entries, key_name, lo, hi, existing, label): continue if not isinstance(entry, dict) or len(entry) != 1: continue - name = list(entry.keys())[0] + name = next(iter(entry)) val = entry[name] if val is None: try: @@ -2007,7 +2007,7 @@ def _assign_to_settings_fields(entries, lo, hi, existing, label): for entry in entries: if not isinstance(entry, dict) or len(entry) != 1: continue - name = list(entry.keys())[0] + name = next(iter(entry)) val = entry[name] if isinstance(val, list): sub_assigned = _assign_to_settings_fields( diff --git a/dependencies.yaml b/dependencies.yaml index 0415e89c76..3bbcdabb89 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -299,7 +299,7 @@ dependencies: packages: - libboost-devel - cpp-argparse - - pyyaml + - &pyyaml pyyaml>=6.0.0 - tbb-devel - zlib - bzip2 @@ -314,7 +314,7 @@ dependencies: - output_types: [conda] packages: - cuda-sanitizer-api - - pyyaml + - *pyyaml test_cpp_cuopt: common: - output_types: [conda] @@ -357,7 +357,7 @@ dependencies: - numba-cuda>=0.22.1 - numba>=0.60.0,<0.65.0 - &pandas pandas>=2.0 - - &pyyaml pyyaml>=6.0.0 + - *pyyaml - scipy>=1.14.1 specific: - output_types: [conda, requirements, pyproject] diff --git a/docs/cuopt/grpc/GRPC_CODE_GENERATION.md b/docs/cuopt/grpc/GRPC_CODE_GENERATION.md index 8d5e64b4ac..97d8d75644 100644 --- a/docs/cuopt/grpc/GRPC_CODE_GENERATION.md +++ b/docs/cuopt/grpc/GRPC_CODE_GENERATION.md @@ -25,7 +25,7 @@ commit the regenerated files. ## File Layout -``` +```text cpp/src/grpc/codegen/ ├── field_registry.yaml # Source of truth for all fields ├── generate_conversions.py # Generator script From 295fb8ff4245a431580e87d8da9733400fb75297 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Thu, 16 Apr 2026 16:11:04 -0400 Subject: [PATCH 03/15] add missing verify_grpc_codegen.sh test script --- ci/verify_grpc_codegen.sh | 66 ++++++++++++++++++++ cpp/src/grpc/codegen/generate_conversions.py | 4 +- 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100755 ci/verify_grpc_codegen.sh diff --git a/ci/verify_grpc_codegen.sh b/ci/verify_grpc_codegen.sh new file mode 100755 index 0000000000..a0f7c2153d --- /dev/null +++ b/ci/verify_grpc_codegen.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Verify that committed codegen output matches what generate_conversions.py produces. +# Fails if a developer edited field_registry.yaml without re-running ./build.sh codegen. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +CODEGEN_DIR="${REPO_DIR}/cpp/src/grpc/codegen" +GENERATED_DIR="${CODEGEN_DIR}/generated" +PROTO_DEST="${REPO_DIR}/cpp/src/grpc/cuopt_remote_data.proto" + +TMPDIR=$(mktemp -d) +trap 'rm -rf ${TMPDIR}' EXIT + +echo "Running code generator into temp directory..." +python "${CODEGEN_DIR}/generate_conversions.py" \ + --registry "${CODEGEN_DIR}/field_registry.yaml" \ + --output-dir "${TMPDIR}" + +echo "Comparing generated output with committed files..." + +FAILED=0 + +for f in "${TMPDIR}"/*; do + fname=$(basename "$f") + committed="${GENERATED_DIR}/${fname}" + if [ ! -f "${committed}" ]; then + echo "MISSING: ${committed} (new generated file not committed)" + FAILED=1 + continue + fi + if ! diff -q "$f" "${committed}" > /dev/null 2>&1; then + echo "MISMATCH: cpp/src/grpc/codegen/generated/${fname}" + diff -u "${committed}" "$f" | head -30 + FAILED=1 + fi +done + +for committed in "${GENERATED_DIR}"/*; do + [ -f "${committed}" ] || continue + fname=$(basename "${committed}") + if [ ! -f "${TMPDIR}/${fname}" ]; then + echo "ORPHANED: cpp/src/grpc/codegen/generated/${fname} (no longer generated)" + FAILED=1 + fi +done + +if [ -f "${TMPDIR}/cuopt_remote_data.proto" ] && [ -f "${PROTO_DEST}" ]; then + if ! diff -q "${TMPDIR}/cuopt_remote_data.proto" "${PROTO_DEST}" > /dev/null 2>&1; then + echo "MISMATCH: cpp/src/grpc/cuopt_remote_data.proto (not copied from codegen/generated)" + FAILED=1 + fi +fi + +if [ ${FAILED} -ne 0 ]; then + echo "" + echo "ERROR: Committed generated files are out of sync with field_registry.yaml." + echo "Run './build.sh codegen' and commit the results." + exit 1 +fi + +echo "OK: All generated files match field_registry.yaml." diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index 407b23742c..75026c7889 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. -# SPDX-License-Identifier: Apache-2.0 -# All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ Code generator for proto definitions, enum converters, and settings, problem, solution conversions. From df4d6d4a69f88f0587ca67c1f9f8dee37d3d8f54 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 29 Apr 2026 17:12:45 -0400 Subject: [PATCH 04/15] address coderabbit feedback on grpc code generator files --- cpp/src/grpc/codegen/field_registry.yaml | 26 +++ cpp/src/grpc/codegen/generate_conversions.py | 206 ++++++++++++++++-- .../generated_enum_converters_problem.inc | 8 +- .../generated_enum_converters_settings.inc | 8 +- .../generated_enum_converters_solution.inc | 8 +- cpp/src/grpc/grpc_problem_mapper.cpp | 1 + cpp/src/grpc/grpc_solution_mapper.cpp | 22 +- 7 files changed, 240 insertions(+), 39 deletions(-) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index 104683c3b8..bbceda31f5 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -145,6 +145,13 @@ enums: # ───────────────────────────────────────────────────────────────────────────── lp_solution: cpp_type: "cpu_lp_solution_t" + # Submessages embedded as fields in LPSolution. Tags are pinned here (rather + # than auto-numbered) so they stay stable across proto schema churn; the + # auto-numberer reserves them out of the arrays/scalars pool. + embeds: + - name: warm_start_data + type: PDLPWarmStartData + field_num: 4 scalars: - lp_termination_status: @@ -340,6 +347,20 @@ mip_solution: - nodes - simplex_iterations +# ───────────────────────────────────────────────────────────────────────────── +# Chunked Result Header +# ───────────────────────────────────────────────────────────────────────────── +# Synthetic message assembled from lp_solution + mip_solution scalars and +# warm-start scalars at codegen time. The only thing the registry needs to +# carry for it is the list of embedded submessages. +# +chunked_result_header: + embeds: + - name: arrays + type: ResultArrayDescriptor + field_num: 50 + repeated: true + # ───────────────────────────────────────────────────────────────────────────── # PDLP Solver Settings # ───────────────────────────────────────────────────────────────────────────── @@ -350,6 +371,11 @@ mip_solution: pdlp_settings: cpp_type: "pdlp_solver_settings_t" proto_type: "cuopt::remote::PDLPSolverSettings" + # Submessages embedded as fields in PDLPSolverSettings. See lp_solution.embeds. + embeds: + - name: warm_start_data + type: PDLPWarmStartData + field_num: 50 fields: # Termination tolerances (nested: settings.tolerances.) diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index 75026c7889..7cd0dfbd05 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -397,10 +397,10 @@ def generate_enum_converters_inc(registry, domain=None): to_fn = _enum_to_proto_fn(key, edef) from_fn = _enum_from_proto_fn(key, edef) prefix = edef.get("proto_prefix", "") - default_cpp = _enum_default(key, edef) - default_proto = _proto_enum_value_name(default_cpp, prefix) - # to_proto + # Omit `default:` so -Wswitch-enum flags any newly-added enumerator that + # was not handled. Throw after the switch to reject unknown wire/int + # values at runtime. lines = [ f"cuopt::remote::{proto_type_name} {to_fn}({cpp_type} v)", "{", @@ -411,11 +411,15 @@ def generate_enum_converters_inc(registry, domain=None): lines.append( f" case {cpp_type}::{cpp_name}: return cuopt::remote::{pname};" ) - lines.append(f" default: return cuopt::remote::{default_proto};") - lines.extend([" }", "}"]) + lines.extend( + [ + " }", + f' throw std::invalid_argument("Unknown {cpp_type}: " + std::to_string(static_cast(v)));', + "}", + ] + ) funcs.append("\n".join(lines)) - # from_proto lines = [ f"{cpp_type} {from_fn}(cuopt::remote::{proto_type_name} v)", "{", @@ -426,8 +430,13 @@ def generate_enum_converters_inc(registry, domain=None): lines.append( f" case cuopt::remote::{pname}: return {cpp_type}::{cpp_name};" ) - lines.append(f" default: return {cpp_type}::{default_cpp};") - lines.extend([" }", "}"]) + lines.extend( + [ + " }", + f' throw std::invalid_argument("Unknown cuopt::remote::{proto_type_name}: " + std::to_string(static_cast(v)));', + "}", + ] + ) funcs.append("\n".join(lines)) return "\n\n".join(funcs) @@ -522,6 +531,25 @@ def generate_array_field_element_size_inc(registry): # ============================================================================ +def _iter_embeds(obj): + """Yield (field_num, proto_line) tuples for every submessage embed declared + on a registry section. Each entry in `obj['embeds']` must carry at least + `name`, `type`, and `field_num`; `repeated: true` emits a `repeated` field. + """ + for embed in obj.get("embeds", []) or []: + try: + num = embed["field_num"] + name = embed["name"] + msg_type = embed["type"] + except (KeyError, TypeError) as exc: + raise ValueError( + f"Malformed embed entry {embed!r}: requires " + "'name', 'type', and 'field_num'" + ) from exc + prefix = "repeated " if embed.get("repeated", False) else "" + yield num, f" {prefix}{msg_type} {name} = {num};" + + def generate_settings_message_proto(registry, message_name, obj): lines = [] for f in parse_settings_fields(obj.get("fields", [])): @@ -530,8 +558,7 @@ def generate_settings_message_proto(registry, message_name, obj): continue ptype = _settings_field_proto_type(registry, f) lines.append((num, f" {ptype} {f['name']} = {num};")) - if message_name == "PDLPSolverSettings": - lines.append((50, " PDLPWarmStartData warm_start_data = 50;")) + lines.extend(_iter_embeds(obj)) lines.sort(key=lambda x: x[0]) return "\n".join(item[1] for item in lines) @@ -649,7 +676,7 @@ def generate_lp_solution_message_proto(registry): num = f.get("field_num") if num is not None: lines.append((num, f" repeated double {f['name']} = {num};")) - lines.append((4, " PDLPWarmStartData warm_start_data = 4;")) + lines.extend(_iter_embeds(obj)) for entry in obj.get("scalars", []): f = parse_field(entry) num = f.get("field_num") @@ -729,7 +756,7 @@ def generate_chunked_result_header_proto(registry): f" {proto_type(f.get('type', 'double'))} {ch_name} = {num};", ) ) - lines.append((50, " repeated ResultArrayDescriptor arrays = 50;")) + lines.extend(_iter_embeds(registry.get("chunked_result_header", {}))) lines.sort(key=lambda x: x[0]) return "\n".join(item[1] for item in lines) @@ -1157,17 +1184,15 @@ def _gen_chunked_to_solution(registry, obj_name, obj, indent=" "): args = [f"std::move({n})" for n in array_names] args += [scalar_vars.get(s, f"_{s}") for s in arg_scalars] - # Warm start - if ws: - ws_arrays = ws.get("arrays", []) + # Warm start — emitted only when the section actually declares arrays. + # Presence is detected by probing the first array's ResultFieldId, so an + # empty arrays list has no sentinel and cannot be decoded here. + if ws and ws.get("arrays"): + ws_arrays = ws["arrays"] ws_rid_prefix = ws.get("result_id_prefix", "") ws_ch_prefix = ws.get("chunked_header_prefix", "") - first_array = parse_field(ws_arrays[0]) if ws_arrays else None - detect_eid = ( - _field_result_id_name(first_array, prefix=ws_rid_prefix) - if first_array - else None - ) + first_array = parse_field(ws_arrays[0]) + detect_eid = _field_result_id_name(first_array, prefix=ws_rid_prefix) lines.append("") lines.append( f"{ind}auto _ws_detect = bytes_to_typed(arrays, cuopt::remote::{detect_eid});" @@ -1766,7 +1791,7 @@ def _gen_chunked_arrays_to_problem(registry, indent=" "): ) lines.append(f"{ind}if (!{name}_str.empty()) {{") lines.append( - f"{ind} cpu_problem.set_{name}({name}_str.data(), static_cast({name}_str.size()));" + f"{ind} cpu_problem.{setter}({name}_str.data(), static_cast({name}_str.size()));" ) lines.append(f"{ind}}}") elif ftype.startswith("repeated"): @@ -1942,6 +1967,129 @@ def _collect_settings_field_nums(entries): return nums +def _embed_field_nums(section): + """Return the set of tags pinned by submessage embeds on a section.""" + nums = set() + for embed in section.get("embeds") or []: + if isinstance(embed, dict) and "field_num" in embed: + nums.add(embed["field_num"]) + return nums + + +def _iter_named_field_nums(entries, key_name="field_num"): + """Yield (num, source_label) for field-list entries carrying key_name.""" + for entry in entries or []: + if isinstance(entry, dict) and len(entry) == 1: + name = next(iter(entry)) + val = entry[name] + if isinstance(val, dict) and key_name in val: + yield val[key_name], name + + +def _iter_named_settings_field_nums(entries, parent=""): + """Yield (num, source_label) for nested settings entries.""" + for entry in entries or []: + if isinstance(entry, dict) and len(entry) == 1: + name = next(iter(entry)) + val = entry[name] + label = f"{parent}.{name}" if parent else name + if isinstance(val, list): + yield from _iter_named_settings_field_nums(val, label) + elif isinstance(val, dict) and "field_num" in val: + yield val["field_num"], label + + +def _iter_named_embed_field_nums(section): + for embed in section.get("embeds") or []: + if ( + isinstance(embed, dict) + and "field_num" in embed + and "name" in embed + ): + yield embed["field_num"], f"embed:{embed['name']}" + + +def _check_unique(label, pairs, errors): + """Record duplicate values found in (num, source) pairs under `label`.""" + seen = {} + for num, src in pairs: + if num in seen: + errors.append( + f"{label}: duplicate tag {num} " + f"(conflict: {seen[num]!r} vs {src!r})" + ) + else: + seen[num] = src + + +def _validate_registry_uniqueness(registry): + """Raise ValueError if the registry assigns the same tag twice within any + proto message's field namespace, or the same array_id twice in the shared + ResultFieldId pool. Aliases declared under `enums.result_field_id.aliases` + are excluded (they are intentional allow_alias entries).""" + errors = [] + + # Per-message field_num pools. Each tuple: (proto message label, section). + per_message = [ + ("OptimizationProblem", "optimization_problem", ["scalars", "arrays"]), + ("LPSolution", "lp_solution", ["scalars", "arrays"]), + ("MIPSolution", "mip_solution", ["scalars", "arrays"]), + ] + for msg, key, field_keys in per_message: + section = registry.get(key) or {} + pairs = [] + for fk in field_keys: + pairs.extend(_iter_named_field_nums(section.get(fk, []))) + pairs.extend(_iter_named_embed_field_nums(section)) + _check_unique(msg, pairs, errors) + + # Settings messages use the nested `fields:` layout. + for msg, key in [ + ("PDLPSolverSettings", "pdlp_settings"), + ("MIPSolverSettings", "mip_settings"), + ]: + section = registry.get(key) or {} + pairs = list( + _iter_named_settings_field_nums(section.get("fields", [])) + ) + pairs.extend(_iter_named_embed_field_nums(section)) + _check_unique(msg, pairs, errors) + + # PDLPWarmStartData is currently sourced from lp_solution.warm_start; if + # mip_solution ever grows a warm_start block it will be the same message + # and tags must not collide with the LP side. + ws_pairs = [] + for key in ["lp_solution", "mip_solution"]: + ws = (registry.get(key) or {}).get("warm_start") or {} + for fk in ["scalars", "arrays"]: + for num, name in _iter_named_field_nums(ws.get(fk, [])): + ws_pairs.append((num, f"{key}.warm_start.{fk}.{name}")) + _check_unique("PDLPWarmStartData", ws_pairs, errors) + + # Global ResultFieldId pool — shared across all solution/warm_start arrays. + # Aliases live under enums.result_field_id.aliases and piggyback on these + # primary numbers via proto's `allow_alias`; they are not part of this pool. + rid_pairs = [] + for key in ["lp_solution", "mip_solution"]: + section = registry.get(key) or {} + for num, name in _iter_named_field_nums( + section.get("arrays", []), "array_id" + ): + rid_pairs.append((num, f"{key}.arrays.{name}")) + ws = section.get("warm_start") or {} + for num, name in _iter_named_field_nums( + ws.get("arrays", []), "array_id" + ): + rid_pairs.append((num, f"{key}.warm_start.arrays.{name}")) + _check_unique("ResultFieldId", rid_pairs, errors) + + if errors: + raise ValueError( + "field_registry.yaml validation failed:\n - " + + "\n - ".join(errors) + ) + + def _ruamel_insert(mapping, key, value): """Add a key to a CommentedMap at its conventional position. @@ -2229,12 +2377,15 @@ def auto_assign_field_numbers(data): scalars, "field_num", lo, hi, existing, f"{section_key}.scalars" ) - # Arrays — field_num (per-message, no cap) + # Arrays — field_num (per-message, no cap). Reserve tags used by + # embedded submessages so the auto-numberer can't hand them out to a + # new array. arrays = section.get("arrays", []) arr_fn_range = FIELD_NUM_RANGES.get( f"{section_key}.arrays.field_num", (1, None) ) existing_fn = _collect_field_nums(arrays, "field_num") + existing_fn |= _embed_field_nums(section) total += _assign_to_field_list( arrays, "field_num", @@ -2302,6 +2453,9 @@ def auto_assign_field_numbers(data): fields = section.get("fields", []) lo, hi = FIELD_NUM_RANGES[section_key] existing = _collect_settings_field_nums(fields) + # Reserve tags used by embedded submessages so auto-numbering won't + # collide with them. + existing |= _embed_field_nums(section) total += _assign_to_settings_fields( fields, lo, hi, existing, section_key ) @@ -2381,6 +2535,12 @@ def main(): ) sys.exit(1) + try: + _validate_registry_uniqueness(registry) + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + sys.exit(1) + outdir = args.output_dir # Proto reference .inc files diff --git a/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc b/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc index 6cb61f3b8d..921a2d3c09 100644 --- a/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc +++ b/cpp/src/grpc/codegen/generated/generated_enum_converters_problem.inc @@ -7,8 +7,8 @@ cuopt::remote::VariableType to_proto_variable_type(var_t v) switch (v) { case var_t::CONTINUOUS: return cuopt::remote::CONTINUOUS; case var_t::INTEGER: return cuopt::remote::INTEGER; - default: return cuopt::remote::CONTINUOUS; } + throw std::invalid_argument("Unknown var_t: " + std::to_string(static_cast(v))); } var_t from_proto_variable_type(cuopt::remote::VariableType v) @@ -16,8 +16,8 @@ var_t from_proto_variable_type(cuopt::remote::VariableType v) switch (v) { case cuopt::remote::CONTINUOUS: return var_t::CONTINUOUS; case cuopt::remote::INTEGER: return var_t::INTEGER; - default: return var_t::CONTINUOUS; } + throw std::invalid_argument("Unknown cuopt::remote::VariableType: " + std::to_string(static_cast(v))); } cuopt::remote::ProblemCategory to_proto_problem_category(problem_category_t v) @@ -25,8 +25,8 @@ cuopt::remote::ProblemCategory to_proto_problem_category(problem_category_t v) switch (v) { case problem_category_t::LP: return cuopt::remote::LP; case problem_category_t::MIP: return cuopt::remote::MIP; - default: return cuopt::remote::LP; } + throw std::invalid_argument("Unknown problem_category_t: " + std::to_string(static_cast(v))); } problem_category_t from_proto_problem_category(cuopt::remote::ProblemCategory v) @@ -34,6 +34,6 @@ problem_category_t from_proto_problem_category(cuopt::remote::ProblemCategory v) switch (v) { case cuopt::remote::LP: return problem_category_t::LP; case cuopt::remote::MIP: return problem_category_t::MIP; - default: return problem_category_t::LP; } + throw std::invalid_argument("Unknown cuopt::remote::ProblemCategory: " + std::to_string(static_cast(v))); } diff --git a/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc b/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc index cd4dad1190..328ef06732 100644 --- a/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_enum_converters_settings.inc @@ -10,8 +10,8 @@ cuopt::remote::PDLPSolverMode to_proto_pdlp_solver_mode(pdlp_solver_mode_t v) case pdlp_solver_mode_t::Methodical1: return cuopt::remote::Methodical1; case pdlp_solver_mode_t::Fast1: return cuopt::remote::Fast1; case pdlp_solver_mode_t::Stable3: return cuopt::remote::Stable3; - default: return cuopt::remote::Stable3; } + throw std::invalid_argument("Unknown pdlp_solver_mode_t: " + std::to_string(static_cast(v))); } pdlp_solver_mode_t from_proto_pdlp_solver_mode(cuopt::remote::PDLPSolverMode v) @@ -22,8 +22,8 @@ pdlp_solver_mode_t from_proto_pdlp_solver_mode(cuopt::remote::PDLPSolverMode v) case cuopt::remote::Methodical1: return pdlp_solver_mode_t::Methodical1; case cuopt::remote::Fast1: return pdlp_solver_mode_t::Fast1; case cuopt::remote::Stable3: return pdlp_solver_mode_t::Stable3; - default: return pdlp_solver_mode_t::Stable3; } + throw std::invalid_argument("Unknown cuopt::remote::PDLPSolverMode: " + std::to_string(static_cast(v))); } cuopt::remote::LPMethod to_proto_lp_method(method_t v) @@ -33,8 +33,8 @@ cuopt::remote::LPMethod to_proto_lp_method(method_t v) case method_t::PDLP: return cuopt::remote::PDLP; case method_t::DualSimplex: return cuopt::remote::DualSimplex; case method_t::Barrier: return cuopt::remote::Barrier; - default: return cuopt::remote::Concurrent; } + throw std::invalid_argument("Unknown method_t: " + std::to_string(static_cast(v))); } method_t from_proto_lp_method(cuopt::remote::LPMethod v) @@ -44,6 +44,6 @@ method_t from_proto_lp_method(cuopt::remote::LPMethod v) case cuopt::remote::PDLP: return method_t::PDLP; case cuopt::remote::DualSimplex: return method_t::DualSimplex; case cuopt::remote::Barrier: return method_t::Barrier; - default: return method_t::Concurrent; } + throw std::invalid_argument("Unknown cuopt::remote::LPMethod: " + std::to_string(static_cast(v))); } diff --git a/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc b/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc index 725e808a55..02f8edc074 100644 --- a/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc +++ b/cpp/src/grpc/codegen/generated/generated_enum_converters_solution.inc @@ -14,8 +14,8 @@ cuopt::remote::PDLPTerminationStatus to_proto_pdlp_termination_status(pdlp_termi case pdlp_termination_status_t::TimeLimit: return cuopt::remote::PDLP_TIME_LIMIT; case pdlp_termination_status_t::ConcurrentLimit: return cuopt::remote::PDLP_CONCURRENT_LIMIT; case pdlp_termination_status_t::PrimalFeasible: return cuopt::remote::PDLP_PRIMAL_FEASIBLE; - default: return cuopt::remote::PDLP_NO_TERMINATION; } + throw std::invalid_argument("Unknown pdlp_termination_status_t: " + std::to_string(static_cast(v))); } pdlp_termination_status_t from_proto_pdlp_termination_status(cuopt::remote::PDLPTerminationStatus v) @@ -30,8 +30,8 @@ pdlp_termination_status_t from_proto_pdlp_termination_status(cuopt::remote::PDLP case cuopt::remote::PDLP_TIME_LIMIT: return pdlp_termination_status_t::TimeLimit; case cuopt::remote::PDLP_CONCURRENT_LIMIT: return pdlp_termination_status_t::ConcurrentLimit; case cuopt::remote::PDLP_PRIMAL_FEASIBLE: return pdlp_termination_status_t::PrimalFeasible; - default: return pdlp_termination_status_t::NoTermination; } + throw std::invalid_argument("Unknown cuopt::remote::PDLPTerminationStatus: " + std::to_string(static_cast(v))); } cuopt::remote::MIPTerminationStatus to_proto_mip_termination_status(mip_termination_status_t v) @@ -44,8 +44,8 @@ cuopt::remote::MIPTerminationStatus to_proto_mip_termination_status(mip_terminat case mip_termination_status_t::Unbounded: return cuopt::remote::MIP_UNBOUNDED; case mip_termination_status_t::TimeLimit: return cuopt::remote::MIP_TIME_LIMIT; case mip_termination_status_t::WorkLimit: return cuopt::remote::MIP_WORK_LIMIT; - default: return cuopt::remote::MIP_NO_TERMINATION; } + throw std::invalid_argument("Unknown mip_termination_status_t: " + std::to_string(static_cast(v))); } mip_termination_status_t from_proto_mip_termination_status(cuopt::remote::MIPTerminationStatus v) @@ -58,6 +58,6 @@ mip_termination_status_t from_proto_mip_termination_status(cuopt::remote::MIPTer case cuopt::remote::MIP_UNBOUNDED: return mip_termination_status_t::Unbounded; case cuopt::remote::MIP_TIME_LIMIT: return mip_termination_status_t::TimeLimit; case cuopt::remote::MIP_WORK_LIMIT: return mip_termination_status_t::WorkLimit; - default: return mip_termination_status_t::NoTermination; } + throw std::invalid_argument("Unknown cuopt::remote::MIPTerminationStatus: " + std::to_string(static_cast(v))); } diff --git a/cpp/src/grpc/grpc_problem_mapper.cpp b/cpp/src/grpc/grpc_problem_mapper.cpp index 2cdf7890d0..9a20050bac 100644 --- a/cpp/src/grpc/grpc_problem_mapper.cpp +++ b/cpp/src/grpc/grpc_problem_mapper.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace cuopt::linear_programming { diff --git a/cpp/src/grpc/grpc_solution_mapper.cpp b/cpp/src/grpc/grpc_solution_mapper.cpp index 5f2acfa595..391a867a54 100644 --- a/cpp/src/grpc/grpc_solution_mapper.cpp +++ b/cpp/src/grpc/grpc_solution_mapper.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include namespace cuopt::linear_programming { @@ -45,24 +47,36 @@ template std::vector bytes_to_typed(const std::map>& arrays, int32_t field_id) { + // An absent entry or empty payload means "field not transmitted" — callers + // use the empty return to distinguish absence. A present-but-misaligned + // payload is corrupt data and must fail loudly instead of silently masking. auto it = arrays.find(field_id); if (it == arrays.end() || it->second.empty()) return {}; - const auto& raw = it->second; + const auto& raw = it->second; + auto check_aligned = [&](size_t elem_size) { + if (raw.size() % elem_size != 0) { + throw std::invalid_argument("bytes_to_typed: payload size " + std::to_string(raw.size()) + + " for field_id " + std::to_string(field_id) + + " is not a multiple of element size " + + std::to_string(elem_size)); + } + }; + if constexpr (std::is_same_v) { - if (raw.size() % sizeof(double) != 0) return {}; + check_aligned(sizeof(double)); size_t n = raw.size() / sizeof(double); std::vector tmp(n); std::memcpy(tmp.data(), raw.data(), n * sizeof(double)); return std::vector(tmp.begin(), tmp.end()); } else if constexpr (std::is_same_v) { - if (raw.size() % sizeof(double) != 0) return {}; + check_aligned(sizeof(double)); size_t n = raw.size() / sizeof(double); std::vector v(n); std::memcpy(v.data(), raw.data(), n * sizeof(double)); return v; } else { - if (raw.size() % sizeof(T) != 0) return {}; + check_aligned(sizeof(T)); size_t n = raw.size() / sizeof(T); std::vector v(n); std::memcpy(v.data(), raw.data(), n * sizeof(T)); From b48a2de51252395313432e3ce4d10783d28ac93a Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 20 May 2026 11:40:00 -0500 Subject: [PATCH 05/15] codegen: address open review comments Apply outstanding CodeRabbit feedback on the gRPC code generator: - Warm-start: detect presence via arrays.count() across all warm-start arrays instead of decoding the first array's bytes. The previous sentinel silently dropped valid payloads when the first warm-start array was empty but companion arrays carried data. - Setter groups (CSR-style): guard on the offsets field rather than values/indices, so zero-nnz sparse matrices are not silently dropped. Also throw on values/indices size mismatch. - Chunked extractors (get_doubles, get_ints): throw on misaligned payload size rather than returning {}, matching bytes_to_typed() in grpc_solution_mapper.cpp. Treating truncated payloads as "absent" let bad uploads be accepted as partial problems. - Validation: add an ArrayFieldId uniqueness pass alongside ResultFieldId. Duplicate array_ids in optimization_problem.arrays would have made chunked uploads ambiguous. - build.sh codegen: delete previously generated files before regenerating, so artifacts no longer emitted by the generator do not linger and cause verify_grpc_codegen.sh to fail. Regenerated artifacts under cpp/src/grpc/codegen/generated/. Signed-off-by: Ramakrishna Prabhu --- build.sh | 5 + cpp/src/grpc/codegen/generate_conversions.py | 129 ++++++++++++++---- .../generated_chunked_arrays_to_problem.inc | 18 ++- .../generated_chunked_to_lp_solution.inc | 6 +- .../generated/generated_proto_to_problem.inc | 10 +- 5 files changed, 131 insertions(+), 37 deletions(-) diff --git a/build.sh b/build.sh index cf92682c29..13841a1674 100755 --- a/build.sh +++ b/build.sh @@ -369,6 +369,11 @@ fi # Regenerate gRPC codegen .inc files from the field registry (explicit target only) if hasArg codegen; then echo "Regenerating codegen .inc files from field_registry.yaml..." + # Remove previously generated files so artifacts no longer emitted by the + # generator do not linger and cause verify_grpc_codegen.sh to fail. + if [ -d "${REPODIR}"/cpp/src/grpc/codegen/generated ]; then + find "${REPODIR}"/cpp/src/grpc/codegen/generated -mindepth 1 -maxdepth 1 -type f -delete + fi python "${REPODIR}"/cpp/src/grpc/codegen/generate_conversions.py \ --registry "${REPODIR}"/cpp/src/grpc/codegen/field_registry.yaml \ --output-dir "${REPODIR}"/cpp/src/grpc/codegen/generated diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index 7cd0dfbd05..59064e68ef 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -300,6 +300,35 @@ def _find_problem_field(obj, name): return None +def _group_presence_field(fields): + """Pick the structural sentinel for a setter group. For CSR-style groups + (values/indices/offsets), offsets is always non-empty even when nnz=0, so + it's the correct presence sentinel. Falls back to the first field.""" + for f in fields: + if f is not None and f["name"].endswith("_offsets"): + return f + return next((f for f in fields if f is not None), None) + + +def _group_companion_pair(fields): + """Identify the (values, indices) pair within a CSR-style setter group, + used to validate that companion arrays have matching lengths. Returns + (values_field, indices_field) or (None, None) if not applicable.""" + values = next( + (f for f in fields if f is not None and f["name"].endswith("_values")), + None, + ) + indices = next( + ( + f + for f in fields + if f is not None and f["name"].endswith("_indices") + ), + None, + ) + return values, indices + + def _field_array_id_name(f): """Derive ArrayFieldId enum name from field name: FIELD_{UPPER_SNAKE(name)}.""" return f"FIELD_{f['name'].upper()}" @@ -1184,34 +1213,33 @@ def _gen_chunked_to_solution(registry, obj_name, obj, indent=" "): args = [f"std::move({n})" for n in array_names] args += [scalar_vars.get(s, f"_{s}") for s in arg_scalars] - # Warm start — emitted only when the section actually declares arrays. - # Presence is detected by probing the first array's ResultFieldId, so an - # empty arrays list has no sentinel and cannot be decoded here. + # Warm start — fire reconstruction if any warm-start array is present in + # the chunked payload. Using arrays.count() (rather than the first array's + # contents) avoids dropping a payload whose first warm-start array happens + # to be empty while others carry data. if ws and ws.get("arrays"): ws_arrays = ws["arrays"] ws_rid_prefix = ws.get("result_id_prefix", "") ws_ch_prefix = ws.get("chunked_header_prefix", "") - first_array = parse_field(ws_arrays[0]) - detect_eid = _field_result_id_name(first_array, prefix=ws_rid_prefix) + detect_eids = [ + _field_result_id_name(parse_field(e), prefix=ws_rid_prefix) + for e in ws_arrays + ] lines.append("") - lines.append( - f"{ind}auto _ws_detect = bytes_to_typed(arrays, cuopt::remote::{detect_eid});" + detect_expr = " || ".join( + f"arrays.count(cuopt::remote::{eid}) != 0" for eid in detect_eids ) - lines.append(f"{ind}if (!_ws_detect.empty()) {{") + lines.append(f"{ind}bool _ws_present = {detect_expr};") + lines.append(f"{ind}if (_ws_present) {{") lines.append(f"{ind} cpu_pdlp_warm_start_data_t ws;") - first = True for entry in ws_arrays: f = parse_field(entry) member = f.get("member", f["name"]) eid = _field_result_id_name(f, prefix=ws_rid_prefix) - if first: - lines.append(f"{ind} ws.{member} = std::move(_ws_detect);") - first = False - else: - lines.append( - f"{ind} ws.{member} = bytes_to_typed(arrays, cuopt::remote::{eid});" - ) + lines.append( + f"{ind} ws.{member} = bytes_to_typed(arrays, cuopt::remote::{eid});" + ) for entry in ws.get("scalars", []): f = parse_field(entry) @@ -1397,17 +1425,19 @@ def _gen_proto_to_problem(registry, indent=" "): lines.append("") - # Setter groups — guard on first field having data - for gname, gdef in setter_groups.items(): + # Setter groups — guard on the structural sentinel (offsets). For CSR-style + # groups, offsets is non-empty even when nnz=0; using values/indices as the + # sentinel would silently drop zero-nnz matrices. + for _gname, gdef in setter_groups.items(): setter_name = gdef["setter"] fields = [ _find_problem_field(obj, fn) for fn in gdef.get("fields", []) ] - first = next((f for f in fields if f is not None), None) - if first is None: + sentinel = _group_presence_field(fields) + if sentinel is None: continue - first_pname = _proto_cpp_name(first["name"]) - lines.append(f"{ind}if (pb_problem.{first_pname}_size() > 0) {{") + sentinel_pname = _proto_cpp_name(sentinel["name"]) + lines.append(f"{ind}if (pb_problem.{sentinel_pname}_size() > 0) {{") ii = ind + " " for f in fields: @@ -1420,6 +1450,16 @@ def _gen_proto_to_problem(registry, indent=" "): f"{ii}std::vector<{cpp_t}> {f['name']}(pb_problem.{pname}().begin(), pb_problem.{pname}().end());" ) + values_f, indices_f = _group_companion_pair(fields) + if values_f is not None and indices_f is not None: + lines.append( + f"{ii}if ({values_f['name']}.size() != {indices_f['name']}.size()) {{" + ) + lines.append( + f'{ii} throw std::invalid_argument("{setter_name}: values/indices size mismatch");' + ) + lines.append(f"{ii}}}") + args = [] for f in fields: if f is None: @@ -1633,9 +1673,13 @@ def _gen_chunked_arrays_to_problem(registry, indent=" "): lines.append( f"{ind} if (it == arrays.end() || it->second.empty()) return {{}};" ) + lines.append(f"{ind} if (it->second.size() % sizeof(double) != 0) {{") lines.append( - f"{ind} if (it->second.size() % sizeof(double) != 0) return {{}};" + f'{ind} throw std::invalid_argument("get_doubles: payload size " + ' + f'std::to_string(it->second.size()) + " for field_id " + ' + f'std::to_string(field_id) + " is not a multiple of sizeof(double)");' ) + lines.append(f"{ind} }}") lines.append(f"{ind} size_t n = it->second.size() / sizeof(double);") lines.append(f"{ind} if constexpr (std::is_same_v) {{") lines.append(f"{ind} std::vector v(n);") @@ -1660,9 +1704,13 @@ def _gen_chunked_arrays_to_problem(registry, indent=" "): lines.append( f"{ind} if (it == arrays.end() || it->second.empty()) return {{}};" ) + lines.append(f"{ind} if (it->second.size() % sizeof(int32_t) != 0) {{") lines.append( - f"{ind} if (it->second.size() % sizeof(int32_t) != 0) return {{}};" + f'{ind} throw std::invalid_argument("get_ints: payload size " + ' + f'std::to_string(it->second.size()) + " for field_id " + ' + f'std::to_string(field_id) + " is not a multiple of sizeof(int32_t)");' ) + lines.append(f"{ind} }}") lines.append(f"{ind} size_t n = it->second.size() / sizeof(int32_t);") lines.append(f"{ind} if constexpr (std::is_same_v) {{") lines.append(f"{ind} std::vector v(n);") @@ -1718,12 +1766,17 @@ def _gen_chunked_arrays_to_problem(registry, indent=" "): lines.append(f"{ind}}};") lines.append("") - # Setter groups - for gname, gdef in setter_groups.items(): + # Setter groups — guard on the structural sentinel (offsets). For CSR-style + # groups, offsets is non-empty even when nnz=0; requiring all companion + # arrays to be non-empty would silently drop zero-nnz matrices. + for _gname, gdef in setter_groups.items(): setter_name = gdef["setter"] fields = [ _find_problem_field(obj, fn) for fn in gdef.get("fields", []) ] + sentinel = _group_presence_field(fields) + if sentinel is None: + continue for f in fields: if f is None: @@ -1735,8 +1788,17 @@ def _gen_chunked_arrays_to_problem(registry, indent=" "): f"{ind}auto {f['name']} = {extract_fn}(cuopt::remote::{afid});" ) - guard_parts = [f"!{f['name']}.empty()" for f in fields if f] - lines.append(f"{ind}if ({' && '.join(guard_parts)}) {{") + lines.append(f"{ind}if (!{sentinel['name']}.empty()) {{") + + values_f, indices_f = _group_companion_pair(fields) + if values_f is not None and indices_f is not None: + lines.append( + f"{ind} if ({values_f['name']}.size() != {indices_f['name']}.size()) {{" + ) + lines.append( + f'{ind} throw std::invalid_argument("{setter_name}: values/indices size mismatch");' + ) + lines.append(f"{ind} }}") args = [] for f in fields: @@ -2083,6 +2145,17 @@ def _validate_registry_uniqueness(registry): rid_pairs.append((num, f"{key}.warm_start.arrays.{name}")) _check_unique("ResultFieldId", rid_pairs, errors) + # ArrayFieldId pool — keys problem-side chunked array payloads. Duplicates + # would make chunked uploads ambiguous (two fields share a payload slot). + problem = registry.get("optimization_problem") or {} + afid_pairs = [ + (num, f"optimization_problem.arrays.{name}") + for num, name in _iter_named_field_nums( + problem.get("arrays", []), "array_id" + ) + ] + _check_unique("ArrayFieldId", afid_pairs, errors) + if errors: raise ValueError( "field_registry.yaml validation failed:\n - " diff --git a/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc b/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc index f3ed757c8d..fa28354b2f 100644 --- a/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc +++ b/cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc @@ -7,7 +7,9 @@ auto get_doubles = [&](int32_t field_id) -> std::vector { auto it = arrays.find(field_id); if (it == arrays.end() || it->second.empty()) return {}; - if (it->second.size() % sizeof(double) != 0) return {}; + if (it->second.size() % sizeof(double) != 0) { + throw std::invalid_argument("get_doubles: payload size " + std::to_string(it->second.size()) + " for field_id " + std::to_string(field_id) + " is not a multiple of sizeof(double)"); + } size_t n = it->second.size() / sizeof(double); if constexpr (std::is_same_v) { std::vector v(n); @@ -23,7 +25,9 @@ auto get_ints = [&](int32_t field_id) -> std::vector { auto it = arrays.find(field_id); if (it == arrays.end() || it->second.empty()) return {}; - if (it->second.size() % sizeof(int32_t) != 0) return {}; + if (it->second.size() % sizeof(int32_t) != 0) { + throw std::invalid_argument("get_ints: payload size " + std::to_string(it->second.size()) + " for field_id " + std::to_string(field_id) + " is not a multiple of sizeof(int32_t)"); + } size_t n = it->second.size() / sizeof(int32_t); if constexpr (std::is_same_v) { std::vector v(n); @@ -61,14 +65,20 @@ auto A_values = get_doubles(cuopt::remote::FIELD_A_VALUES); auto A_indices = get_ints(cuopt::remote::FIELD_A_INDICES); auto A_offsets = get_ints(cuopt::remote::FIELD_A_OFFSETS); - if (!A_values.empty() && !A_indices.empty() && !A_offsets.empty()) { + if (!A_offsets.empty()) { + if (A_values.size() != A_indices.size()) { + throw std::invalid_argument("set_csr_constraint_matrix: values/indices size mismatch"); + } cpu_problem.set_csr_constraint_matrix(A_values.data(), static_cast(A_values.size()), A_indices.data(), static_cast(A_indices.size()), A_offsets.data(), static_cast(A_offsets.size())); } auto Q_values = get_doubles(cuopt::remote::FIELD_Q_VALUES); auto Q_indices = get_ints(cuopt::remote::FIELD_Q_INDICES); auto Q_offsets = get_ints(cuopt::remote::FIELD_Q_OFFSETS); - if (!Q_values.empty() && !Q_indices.empty() && !Q_offsets.empty()) { + if (!Q_offsets.empty()) { + if (Q_values.size() != Q_indices.size()) { + throw std::invalid_argument("set_quadratic_objective_matrix: values/indices size mismatch"); + } cpu_problem.set_quadratic_objective_matrix(Q_values.data(), static_cast(Q_values.size()), Q_indices.data(), static_cast(Q_indices.size()), Q_offsets.data(), static_cast(Q_offsets.size())); } diff --git a/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc b/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc index 339093ec36..8e3935d1b6 100644 --- a/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc +++ b/cpp/src/grpc/codegen/generated/generated_chunked_to_lp_solution.inc @@ -16,10 +16,10 @@ auto _solve_time = static_cast(h.solve_time()); auto _solved_by = static_cast(h.solved_by()); - auto _ws_detect = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_PRIMAL_SOLUTION); - if (!_ws_detect.empty()) { + bool _ws_present = arrays.count(cuopt::remote::RESULT_WS_CURRENT_PRIMAL_SOLUTION) != 0 || arrays.count(cuopt::remote::RESULT_WS_CURRENT_DUAL_SOLUTION) != 0 || arrays.count(cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVERAGE) != 0 || arrays.count(cuopt::remote::RESULT_WS_INITIAL_DUAL_AVERAGE) != 0 || arrays.count(cuopt::remote::RESULT_WS_CURRENT_ATY) != 0 || arrays.count(cuopt::remote::RESULT_WS_SUM_PRIMAL_SOLUTIONS) != 0 || arrays.count(cuopt::remote::RESULT_WS_SUM_DUAL_SOLUTIONS) != 0 || arrays.count(cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION) != 0 || arrays.count(cuopt::remote::RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION) != 0; + if (_ws_present) { cpu_pdlp_warm_start_data_t ws; - ws.current_primal_solution_ = std::move(_ws_detect); + ws.current_primal_solution_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_PRIMAL_SOLUTION); ws.current_dual_solution_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_CURRENT_DUAL_SOLUTION); ws.initial_primal_average_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_INITIAL_PRIMAL_AVERAGE); ws.initial_dual_average_ = bytes_to_typed(arrays, cuopt::remote::RESULT_WS_INITIAL_DUAL_AVERAGE); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc index 77c5a54e0d..7fd7c8c2ce 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc @@ -8,17 +8,23 @@ cpu_problem.set_objective_scaling_factor(pb_problem.objective_scaling_factor()); cpu_problem.set_objective_offset(pb_problem.objective_offset()); - if (pb_problem.a_values_size() > 0) { + if (pb_problem.a_offsets_size() > 0) { std::vector A_values(pb_problem.a_values().begin(), pb_problem.a_values().end()); std::vector A_indices(pb_problem.a_indices().begin(), pb_problem.a_indices().end()); std::vector A_offsets(pb_problem.a_offsets().begin(), pb_problem.a_offsets().end()); + if (A_values.size() != A_indices.size()) { + throw std::invalid_argument("set_csr_constraint_matrix: values/indices size mismatch"); + } cpu_problem.set_csr_constraint_matrix(A_values.data(), static_cast(A_values.size()), A_indices.data(), static_cast(A_indices.size()), A_offsets.data(), static_cast(A_offsets.size())); } - if (pb_problem.q_values_size() > 0) { + if (pb_problem.q_offsets_size() > 0) { std::vector Q_values(pb_problem.q_values().begin(), pb_problem.q_values().end()); std::vector Q_indices(pb_problem.q_indices().begin(), pb_problem.q_indices().end()); std::vector Q_offsets(pb_problem.q_offsets().begin(), pb_problem.q_offsets().end()); + if (Q_values.size() != Q_indices.size()) { + throw std::invalid_argument("set_quadratic_objective_matrix: values/indices size mismatch"); + } cpu_problem.set_quadratic_objective_matrix(Q_values.data(), static_cast(Q_values.size()), Q_indices.data(), static_cast(Q_indices.size()), Q_offsets.data(), static_cast(Q_offsets.size())); } From c412e710051a62f285921d5f6aec855e35b492eb Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 26 May 2026 15:27:37 -0400 Subject: [PATCH 06/15] miscellaneous tweaks to grpc codegen * remove redundant proto file * update comments * audit behavior against hand-written code --- build.sh | 2 - ci/verify_grpc_codegen.sh | 8 - cpp/src/grpc/codegen/field_registry.yaml | 69 +++-- cpp/src/grpc/codegen/generate_conversions.py | 34 +-- .../generated_array_field_element_size.inc | 13 +- cpp/src/grpc/cuopt_remote_data.proto | 289 ------------------ cpp/src/grpc/grpc_settings_mapper.cpp | 35 +++ docs/cuopt/grpc/GRPC_CODE_GENERATION.md | 18 +- 8 files changed, 119 insertions(+), 349 deletions(-) delete mode 100644 cpp/src/grpc/cuopt_remote_data.proto diff --git a/build.sh b/build.sh index 664acb995d..7b7c943b83 100755 --- a/build.sh +++ b/build.sh @@ -352,8 +352,6 @@ if hasArg codegen; then python "${REPODIR}"/cpp/src/grpc/codegen/generate_conversions.py \ --registry "${REPODIR}"/cpp/src/grpc/codegen/field_registry.yaml \ --output-dir "${REPODIR}"/cpp/src/grpc/codegen/generated - cp "${REPODIR}"/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto \ - "${REPODIR}"/cpp/src/grpc/cuopt_remote_data.proto echo "Done. Remember to commit the generated files." fi diff --git a/ci/verify_grpc_codegen.sh b/ci/verify_grpc_codegen.sh index a0f7c2153d..46755c030b 100755 --- a/ci/verify_grpc_codegen.sh +++ b/ci/verify_grpc_codegen.sh @@ -11,7 +11,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" CODEGEN_DIR="${REPO_DIR}/cpp/src/grpc/codegen" GENERATED_DIR="${CODEGEN_DIR}/generated" -PROTO_DEST="${REPO_DIR}/cpp/src/grpc/cuopt_remote_data.proto" TMPDIR=$(mktemp -d) trap 'rm -rf ${TMPDIR}' EXIT @@ -49,13 +48,6 @@ for committed in "${GENERATED_DIR}"/*; do fi done -if [ -f "${TMPDIR}/cuopt_remote_data.proto" ] && [ -f "${PROTO_DEST}" ]; then - if ! diff -q "${TMPDIR}/cuopt_remote_data.proto" "${PROTO_DEST}" > /dev/null 2>&1; then - echo "MISMATCH: cpp/src/grpc/cuopt_remote_data.proto (not copied from codegen/generated)" - FAILED=1 - fi -fi - if [ ${FAILED} -ne 0 ]; then echo "" echo "ERROR: Committed generated files are out of sync with field_registry.yaml." diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index 9f6324b874..b0512b1957 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -3,20 +3,31 @@ # # Field registry for auto-generating proto definitions and C++ conversion code. # -# Convention-over-configuration: -# - Bare field name = double type, getter = get_() -# - "field: type" = specified type -# - "field: { type: T, ... }" = full override +# Convention-over-configuration. Each entry under scalars: / arrays: can be +# written in any of three forms (see parse_field in generate_conversions.py): +# - Bare name – defaults to double for scalars, repeated double for arrays +# - "name: type" – specified type +# - "name: { type: T, ... }" – full override # -# Scalar getters default to get_(). -# Array getters default to get__host() (for optimization_problem) or -# get__host() for solution arrays. +# Default getters: scalars → get_(); arrays → get__host() (both +# problem and solution arrays). +# Default setters: set_(). # Proto field name = registry field name (always). # Enum conventions: see the "Enums" section below for full defaults. # # Field numbers: -# field_num – proto message field number (auto-assigned if missing) -# array_id – enum value for ArrayFieldId or ResultFieldId (auto-assigned) +# field_num – proto message field tag (scoped to its proto message). Used +# by the unary path (SubmitJob / GetResult), where the whole +# problem or solution fits in one proto message. +# array_id – enum value for ArrayFieldId (problem) or ResultFieldId +# (solution + warm-start). Used by the chunked path +# (SendArrayChunk / GetResultChunk) to route each chunk back +# to the right array — independent of any proto message. +# +# Both are normally assigned manually. Passing --auto-number to the generator +# fills in missing values. Without --auto-number, a fully-stripped registry is +# an error and a partially-stripped one will silently emit broken code for +# the unassigned entries. # # Numbering is contiguous within each proto message (no artificial caps). # The only hard ranges are for solution scalars that share ChunkedResultHeader: @@ -24,12 +35,12 @@ # MIP: 2000-2999 # WS: 3000-3999 # -# Attributes (all optional): +# Attributes (all optional unless noted): # # Per-field: # type – proto wire type (default: double for scalars, repeated double for arrays) -# field_num – proto message field number (auto-assigned if missing) -# array_id – enum value for ArrayFieldId or ResultFieldId (auto-assigned) +# field_num – proto message field number (see "Field numbers" above) +# array_id – enum value for ArrayFieldId or ResultFieldId # setter_getter_root – C++ getter/setter root when different from field name # getter – explicit C++ getter expression (overrides setter_getter_root) # setter – explicit C++ setter name @@ -47,6 +58,15 @@ # constructor_args – mapping of constructor parameter names to field sources # presence_check – predicate expression to test if optional data is present # getter – (per-section) expression to access the section's C++ data +# embeds – list of submessages embedded as fields (e.g. PDLPWarmStartData) +# proto_type – fully-qualified C++ proto type (settings sections) +# proto_message – proto message name (optimization_problem) +# fields – list of fields (settings sections; supports nested sub-structs) +# setter_groups – multi-array C++ setters keyed by group name (optimization_problem) +# +# Warm-start sub-section adds: +# result_id_prefix – prefix for derived RESULT__ ids +# chunked_header_prefix – prefix applied to scalar field names in ChunkedResultHeader # # See GRPC_CODE_GENERATION.md for the full specification and examples. @@ -54,14 +74,20 @@ # Enums # ───────────────────────────────────────────────────────────────────────────── # -# Convention-based defaults (override any field explicitly when needed): -# cpp_type — {key}_t -# proto_type — PascalCase from key (acronyms PDLP/MIP/LP/QP/VRP/PDP uppercased) -# to_proto_fn — to_proto_{key}() -# from_proto — from_proto_{key}() -# default — first value (proto3 zero-value) -# values — bare names auto-number from 0; {Name: N} resets counter to N -# proto_prefix — "" (no prefix); set when proto values need disambiguation +# Attributes (override any field explicitly when needed): +# domain — required: one of `problem`, `settings`, `solution`. Controls +# which generated_enum_converters_*.inc file the converters +# land in. +# cpp_type — {key}_t +# proto_type — PascalCase from key (acronyms PDLP/MIP/LP/QP/VRP/PDP uppercased) +# to_proto_fn — to_proto_{key}() +# from_proto_fn — from_proto_{key}() +# default — first value (proto3 zero-value) +# values — bare names auto-number from 0; {Name: N} resets counter to N +# proto_prefix — "" (no prefix); set when proto values need disambiguation +# aliases — optional mapping of additional enum value names to numeric +# ids; used by the auto-derived `result_field_id` enum to +# preserve legacy abbreviated names alongside canonical ones # # Proto value names: proto_prefix + UPPER_SNAKE(CppName), or just CppName if # no prefix. @@ -128,8 +154,7 @@ enums: # ResultFieldId is auto-derived from solution/warm-start array_id fields. # Aliases here preserve backward compatibility with upstream's abbreviated - # rapids-pre-commit-hooks: disable-next-line - # names (26.04) while the codegen uses full, consistent canonical names. + # legacy names while the codegen uses full, consistent canonical names. result_field_id: aliases: RESULT_WS_CURRENT_PRIMAL: 3 diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index 59064e68ef..d56b020a41 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -122,19 +122,6 @@ def camel_to_upper_snake(name): return s.upper() -def proto_type(t): - """Map YAML type to proto type.""" - return { - "double": "double", - "float": "float", - "int32": "int32", - "int64": "int64", - "bool": "bool", - "string": "string", - "bytes": "bytes", - }.get(t, t) - - def _from_proto_cast(ftype): """Return C++ cast for reading a proto field back into template type.""" if ftype in ("double", "float"): @@ -359,7 +346,7 @@ def _problem_field_proto_type(registry, ftype): edef = _lookup_enum(registry, ftype) if edef: return _enum_proto_type(ftype, edef) - return proto_type(ftype) + return ftype def _settings_field_proto_type(registry, f): @@ -368,7 +355,7 @@ def _settings_field_proto_type(registry, f): edef = _lookup_enum(registry, ftype) if edef: return _enum_proto_type(ftype, edef) - return proto_type(ftype) + return ftype def _solution_scalar_proto_type(registry, f): @@ -377,7 +364,7 @@ def _solution_scalar_proto_type(registry, f): edef = _lookup_enum(registry, ftype) if edef: return _enum_proto_type(ftype, edef) - return proto_type(ftype) + return ftype # ============================================================================ @@ -535,7 +522,12 @@ def generate_array_field_id_enum(registry): def generate_array_field_element_size_inc(registry): - """Generate body of array_field_element_size() switch function.""" + """Generate body of array_field_element_size() switch function. + + Emits every known ArrayFieldId case explicitly and returns -1 after the + switch for unrecognized values, rather than a default case. A default + case would silently coerce an unknown field id to some size and mask + enum-vs-code drift; the -1 sentinel lets callers detect the mismatch.""" obj = registry.get("optimization_problem", {}) cases_by_size = {} for entry in obj.get("arrays", []): @@ -545,13 +537,11 @@ def generate_array_field_element_size_inc(registry): cases_by_size.setdefault(size, []).append(afid) lines = [" switch (field_id) {"] for size in sorted(cases_by_size.keys()): - if size == 8: - continue for afid in cases_by_size[size]: lines.append(f" case cuopt::remote::{afid}:") lines.append(f" return {size};") - lines.append(" default: return 8;") lines.append(" }") + lines.append(" return -1;") return "\n".join(lines) @@ -690,7 +680,7 @@ def generate_warm_start_message_proto(registry): lines.append( ( num, - f" {proto_type(f.get('type', 'double'))} {f['name']} = {num};", + f" {f.get('type', 'double')} {f['name']} = {num};", ) ) lines.sort(key=lambda x: x[0]) @@ -782,7 +772,7 @@ def generate_chunked_result_header_proto(registry): lines.append( ( num, - f" {proto_type(f.get('type', 'double'))} {ch_name} = {num};", + f" {f.get('type', 'double')} {ch_name} = {num};", ) ) lines.extend(_iter_embeds(registry.get("chunked_result_header", {}))) diff --git a/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc b/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc index b1eb078ccd..5dbe2a8c92 100644 --- a/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc +++ b/cpp/src/grpc/codegen/generated/generated_array_field_element_size.inc @@ -13,5 +13,16 @@ case cuopt::remote::FIELD_Q_INDICES: case cuopt::remote::FIELD_Q_OFFSETS: return 4; - default: return 8; + case cuopt::remote::FIELD_A_VALUES: + case cuopt::remote::FIELD_C: + case cuopt::remote::FIELD_B: + case cuopt::remote::FIELD_VARIABLE_LOWER_BOUNDS: + case cuopt::remote::FIELD_VARIABLE_UPPER_BOUNDS: + case cuopt::remote::FIELD_CONSTRAINT_LOWER_BOUNDS: + case cuopt::remote::FIELD_CONSTRAINT_UPPER_BOUNDS: + case cuopt::remote::FIELD_INITIAL_PRIMAL_SOLUTION: + case cuopt::remote::FIELD_INITIAL_DUAL_SOLUTION: + case cuopt::remote::FIELD_Q_VALUES: + return 8; } + return -1; diff --git a/cpp/src/grpc/cuopt_remote_data.proto b/cpp/src/grpc/cuopt_remote_data.proto deleted file mode 100644 index 11a817d2e7..0000000000 --- a/cpp/src/grpc/cuopt_remote_data.proto +++ /dev/null @@ -1,289 +0,0 @@ -// AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml -// DO NOT EDIT — regenerate with: python cpp/src/grpc/codegen/generate_conversions.py - -syntax = "proto3"; - -package cuopt.remote; - -enum PDLPTerminationStatus { - PDLP_NO_TERMINATION = 0; - PDLP_NUMERICAL_ERROR = 1; - PDLP_OPTIMAL = 2; - PDLP_PRIMAL_INFEASIBLE = 3; - PDLP_DUAL_INFEASIBLE = 4; - PDLP_ITERATION_LIMIT = 5; - PDLP_TIME_LIMIT = 6; - PDLP_CONCURRENT_LIMIT = 7; - PDLP_PRIMAL_FEASIBLE = 8; -} - -enum MIPTerminationStatus { - MIP_NO_TERMINATION = 0; - MIP_OPTIMAL = 1; - MIP_FEASIBLE_FOUND = 2; - MIP_INFEASIBLE = 3; - MIP_UNBOUNDED = 4; - MIP_TIME_LIMIT = 5; - MIP_WORK_LIMIT = 6; -} - -enum PDLPSolverMode { - Stable1 = 0; - Stable2 = 1; - Methodical1 = 2; - Fast1 = 3; - Stable3 = 4; -} - -enum LPMethod { - Concurrent = 0; - PDLP = 1; - DualSimplex = 2; - Barrier = 3; -} - -enum VariableType { - CONTINUOUS = 0; - INTEGER = 1; - SEMI_CONTINUOUS = 2; -} - -enum ProblemCategory { - LP = 0; - MIP = 1; -} - -enum ResultFieldId { - option allow_alias = true; - RESULT_PRIMAL_SOLUTION = 0; - RESULT_DUAL_SOLUTION = 1; - RESULT_REDUCED_COST = 2; - RESULT_WS_CURRENT_PRIMAL_SOLUTION = 3; - RESULT_WS_CURRENT_DUAL_SOLUTION = 4; - RESULT_WS_INITIAL_PRIMAL_AVERAGE = 5; - RESULT_WS_INITIAL_DUAL_AVERAGE = 6; - RESULT_WS_CURRENT_ATY = 7; - RESULT_WS_SUM_PRIMAL_SOLUTIONS = 8; - RESULT_WS_SUM_DUAL_SOLUTIONS = 9; - RESULT_WS_LAST_RESTART_DUALITY_GAP_PRIMAL_SOLUTION = 10; - RESULT_WS_LAST_RESTART_DUALITY_GAP_DUAL_SOLUTION = 11; - RESULT_MIP_SOLUTION = 12; - RESULT_WS_CURRENT_PRIMAL = 3; - RESULT_WS_CURRENT_DUAL = 4; - RESULT_WS_INITIAL_PRIMAL_AVG = 5; - RESULT_WS_INITIAL_DUAL_AVG = 6; - RESULT_WS_SUM_PRIMAL = 8; - RESULT_WS_SUM_DUAL = 9; - RESULT_WS_LAST_RESTART_GAP_PRIMAL = 10; - RESULT_WS_LAST_RESTART_GAP_DUAL = 11; -} - -enum ArrayFieldId { - FIELD_VARIABLE_NAMES = 0; - FIELD_ROW_NAMES = 1; - FIELD_A_VALUES = 2; - FIELD_A_INDICES = 3; - FIELD_A_OFFSETS = 4; - FIELD_C = 5; - FIELD_B = 6; - FIELD_VARIABLE_LOWER_BOUNDS = 7; - FIELD_VARIABLE_UPPER_BOUNDS = 8; - FIELD_CONSTRAINT_LOWER_BOUNDS = 9; - FIELD_CONSTRAINT_UPPER_BOUNDS = 10; - FIELD_ROW_TYPES = 11; - FIELD_VARIABLE_TYPES = 12; - FIELD_INITIAL_PRIMAL_SOLUTION = 13; - FIELD_INITIAL_DUAL_SOLUTION = 14; - FIELD_Q_VALUES = 15; - FIELD_Q_INDICES = 16; - FIELD_Q_OFFSETS = 17; -} - -message OptimizationProblem { - string problem_name = 1; - string objective_name = 2; - bool maximize = 3; - double objective_scaling_factor = 4; - double objective_offset = 5; - repeated string variable_names = 7; - repeated string row_names = 8; - repeated double A_values = 9; - repeated int32 A_indices = 10; - repeated int32 A_offsets = 11; - repeated double c = 12; - repeated double b = 13; - repeated double variable_lower_bounds = 14; - repeated double variable_upper_bounds = 15; - repeated double constraint_lower_bounds = 16; - repeated double constraint_upper_bounds = 17; - bytes row_types = 18; - repeated VariableType variable_types = 19; - repeated double initial_primal_solution = 20; - repeated double initial_dual_solution = 21; - repeated double Q_values = 22; - repeated int32 Q_indices = 23; - repeated int32 Q_offsets = 24; -} - -message PDLPSolverSettings { - double absolute_gap_tolerance = 1; - double relative_gap_tolerance = 2; - double primal_infeasible_tolerance = 3; - double dual_infeasible_tolerance = 4; - double absolute_dual_tolerance = 5; - double relative_dual_tolerance = 6; - double absolute_primal_tolerance = 7; - double relative_primal_tolerance = 8; - double time_limit = 9; - int64 iteration_limit = 10; - bool log_to_console = 11; - bool detect_infeasibility = 12; - bool strict_infeasibility = 13; - PDLPSolverMode pdlp_solver_mode = 14; - LPMethod method = 15; - int32 presolver = 16; - bool dual_postsolve = 17; - bool crossover = 18; - int32 num_gpus = 19; - bool per_constraint_residual = 20; - bool cudss_deterministic = 21; - int32 folding = 22; - int32 augmented = 23; - int32 dualize = 24; - int32 ordering = 25; - int32 barrier_dual_initial_point = 26; - bool eliminate_dense_columns = 27; - bool save_best_primal_so_far = 28; - bool first_primal_feasible = 29; - int32 pdlp_precision = 30; - PDLPWarmStartData warm_start_data = 50; -} - -message MIPSolverSettings { - double time_limit = 1; - double relative_mip_gap = 2; - double absolute_mip_gap = 3; - double integrality_tolerance = 4; - double absolute_tolerance = 5; - double relative_tolerance = 6; - double presolve_absolute_tolerance = 7; - bool log_to_console = 8; - bool heuristics_only = 9; - int32 num_cpu_threads = 10; - int32 num_gpus = 11; - int32 presolver = 12; - int32 mip_scaling = 13; - double work_limit = 14; - int32 node_limit = 15; - int32 reliability_branching = 16; - int32 mip_batch_pdlp_strong_branching = 17; - int32 max_cut_passes = 18; - int32 mir_cuts = 19; - int32 mixed_integer_gomory_cuts = 20; - int32 knapsack_cuts = 21; - int32 clique_cuts = 22; - int32 strong_chvatal_gomory_cuts = 23; - int32 reduced_cost_strengthening = 24; - double cut_change_threshold = 25; - double cut_min_orthogonality = 26; - int32 determinism_mode = 27; - int32 seed = 28; - bool probing = 29; - int32 strong_branching_simplex_iteration_limit = 30; - int32 implied_bound_cuts = 31; - int32 mip_batch_pdlp_reliability_branching = 32; -} - -message PDLPWarmStartData { - repeated double current_primal_solution = 1; - repeated double current_dual_solution = 2; - repeated double initial_primal_average = 3; - repeated double initial_dual_average = 4; - repeated double current_ATY = 5; - repeated double sum_primal_solutions = 6; - repeated double sum_dual_solutions = 7; - repeated double last_restart_duality_gap_primal_solution = 8; - repeated double last_restart_duality_gap_dual_solution = 9; - double initial_primal_weight = 3000; - double initial_step_size = 3001; - int32 total_pdlp_iterations = 3002; - int32 total_pdhg_iterations = 3003; - double last_candidate_kkt_score = 3004; - double last_restart_kkt_score = 3005; - double sum_solution_weight = 3006; - int32 iterations_since_last_restart = 3007; -} - -message LPSolution { - repeated double primal_solution = 1; - repeated double dual_solution = 2; - repeated double reduced_cost = 3; - PDLPWarmStartData warm_start_data = 4; - PDLPTerminationStatus lp_termination_status = 1000; - string error_message = 1001; - double l2_primal_residual = 1002; - double l2_dual_residual = 1003; - double primal_objective = 1004; - double dual_objective = 1005; - double gap = 1006; - int32 nb_iterations = 1007; - double solve_time = 1008; - int32 solved_by = 1009; -} - -message MIPSolution { - repeated double mip_solution = 1; - MIPTerminationStatus mip_termination_status = 2000; - string mip_error_message = 2001; - double mip_objective = 2002; - double mip_gap = 2003; - double solution_bound = 2004; - double total_solve_time = 2005; - double presolve_time = 2006; - double max_constraint_violation = 2007; - double max_int_violation = 2008; - double max_variable_bound_violation = 2009; - int32 nodes = 2010; - int32 simplex_iterations = 2011; -} - -message ResultArrayDescriptor { - ResultFieldId field_id = 1; - int64 total_elements = 2; - int64 element_size_bytes = 3; -} - -message ChunkedResultHeader { - ProblemCategory problem_category = 1; - repeated ResultArrayDescriptor arrays = 50; - PDLPTerminationStatus lp_termination_status = 1000; - string error_message = 1001; - double l2_primal_residual = 1002; - double l2_dual_residual = 1003; - double primal_objective = 1004; - double dual_objective = 1005; - double gap = 1006; - int32 nb_iterations = 1007; - double solve_time = 1008; - int32 solved_by = 1009; - MIPTerminationStatus mip_termination_status = 2000; - string mip_error_message = 2001; - double mip_objective = 2002; - double mip_gap = 2003; - double solution_bound = 2004; - double total_solve_time = 2005; - double presolve_time = 2006; - double max_constraint_violation = 2007; - double max_int_violation = 2008; - double max_variable_bound_violation = 2009; - int32 nodes = 2010; - int32 simplex_iterations = 2011; - double ws_initial_primal_weight = 3000; - double ws_initial_step_size = 3001; - int32 ws_total_pdlp_iterations = 3002; - int32 ws_total_pdhg_iterations = 3003; - double ws_last_candidate_kkt_score = 3004; - double ws_last_restart_kkt_score = 3005; - double ws_sum_solution_weight = 3006; - int32 ws_iterations_since_last_restart = 3007; -} diff --git a/cpp/src/grpc/grpc_settings_mapper.cpp b/cpp/src/grpc/grpc_settings_mapper.cpp index 1d341b6fc3..5f4303a7a2 100644 --- a/cpp/src/grpc/grpc_settings_mapper.cpp +++ b/cpp/src/grpc/grpc_settings_mapper.cpp @@ -33,6 +33,26 @@ void map_proto_to_pdlp_settings(const cuopt::remote::PDLPSolverSettings& pb_sett pdlp_solver_settings_t& settings) { #include "generated_proto_to_pdlp_settings.inc" + + // Post-decode input sanitization: the generated code does raw static_cast + // on int32 -> enum, which is UB for values outside the enum range. Clamp + // out-of-range values from buggy/untrusted encoders to safe defaults, and + // guard the int64 -> i_t conversion of iteration_limit against overflow. + { + auto pv = pb_settings.presolver(); + if (pv < CUOPT_PRESOLVE_DEFAULT || pv > CUOPT_PRESOLVE_PSLP) { + settings.presolver = presolver_t::Default; + } + } + { + auto pv = pb_settings.pdlp_precision(); + if (pv < CUOPT_PDLP_DEFAULT_PRECISION || pv > CUOPT_PDLP_MIXED_PRECISION) { + settings.pdlp_precision = pdlp_precision_t::DefaultPrecision; + } + } + if (pb_settings.iteration_limit() > static_cast(std::numeric_limits::max())) { + settings.iteration_limit = std::numeric_limits::max(); + } } template @@ -47,6 +67,21 @@ void map_proto_to_mip_settings(const cuopt::remote::MIPSolverSettings& pb_settin mip_solver_settings_t& settings) { #include "generated_proto_to_mip_settings.inc" + + // Post-decode input sanitization: clamp out-of-range enum / mode values + // from buggy/untrusted encoders to safe defaults. + { + auto pv = pb_settings.presolver(); + if (pv < CUOPT_PRESOLVE_DEFAULT || pv > CUOPT_PRESOLVE_PSLP) { + settings.presolver = presolver_t::Default; + } + } + { + auto sv = pb_settings.mip_scaling(); + if (sv < CUOPT_MIP_SCALING_OFF || sv > CUOPT_MIP_SCALING_NO_OBJECTIVE) { + settings.mip_scaling = CUOPT_MIP_SCALING_ON; + } + } } // Explicit template instantiations diff --git a/docs/cuopt/grpc/GRPC_CODE_GENERATION.md b/docs/cuopt/grpc/GRPC_CODE_GENERATION.md index 97d8d75644..77e90b7aae 100644 --- a/docs/cuopt/grpc/GRPC_CODE_GENERATION.md +++ b/docs/cuopt/grpc/GRPC_CODE_GENERATION.md @@ -325,9 +325,11 @@ Warm start field names match the C++ struct member names directly (e.g. attribute is only needed if the proto field name cannot match the C++ name due to ambiguity. -Warm start detection during chunked deserialization is auto-derived: if the -first array in the warm start section is present (non-empty), warm start data -is considered present. +Warm start detection during chunked deserialization is auto-derived: warm +start data is reconstructed whenever any warm-start array is present in the +chunked payload. The generator emits `arrays.count(...) != 0` checks OR'd +across every warm-start array — so a payload whose first warm-start array +happens to be empty but others carry data is still picked up. --- @@ -497,8 +499,14 @@ Arrays that belong to a setter group are excluded from normal per-field deserialization and handled as a batch instead. During deserialization, the generator automatically guards setter group calls -by checking if the first field has data (e.g. `if (pb_problem.a_values_size() > 0)`). -This is derived from the group structure — no explicit condition attribute is needed. +on the structural sentinel field — preferring any field whose name ends with +`_offsets` (so CSR-style groups still trigger when nnz=0, because `*_offsets` +is non-empty even when `*_values`/`*_indices` are empty) and falling back to +the first field otherwise. The generated guard looks like +`if (pb_problem.a_offsets_size() > 0)`. For groups that contain +`_values`/`_indices` pairs, the generator additionally emits a size-mismatch +check that throws `std::invalid_argument` if the companion arrays disagree — +no explicit condition attribute is needed in the registry. --- From 877e05a4e7ecf835e6d2d3a9b62a0beb3e937cbb Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 26 May 2026 15:58:05 -0400 Subject: [PATCH 07/15] add grpc transport for mip_settings.symmetry --- cpp/src/grpc/codegen/field_registry.yaml | 7 +++++ .../codegen/generated/cuopt_remote_data.proto | 1 + .../generated_mip_settings_to_proto.inc | 1 + .../generated_proto_to_mip_settings.inc | 1 + cpp/src/grpc/grpc_settings_mapper.cpp | 6 ++++ .../grpc/grpc_client_test.cpp | 30 +++++++++++++++++++ 6 files changed, 46 insertions(+) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index b0512b1957..18bbafed23 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -550,6 +550,13 @@ mip_settings: - mip_scaling: field_num: 13 type: int32 + - symmetry: + # Symmetry-detection level (dejavu). Valid: -1 (default), 0 (off), + # 1 (orbital fixing), 2 (orbital fixing + lexical reduction). The + # mapper clamps out-of-range values to -1 to match the local-solve + # range check in cpp/src/math_optimization/solver_settings.cu. + field_num: 33 + type: int32 # Additional limits - work_limit: diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 11a817d2e7..5f53e4ee99 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -192,6 +192,7 @@ message MIPSolverSettings { int32 strong_branching_simplex_iteration_limit = 30; int32 implied_bound_cuts = 31; int32 mip_batch_pdlp_reliability_branching = 32; + int32 symmetry = 33; } message PDLPWarmStartData { diff --git a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc index b930de6ed9..8993330a69 100644 --- a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc +++ b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc @@ -15,6 +15,7 @@ pb_settings->set_num_gpus(settings.num_gpus); pb_settings->set_presolver(static_cast(settings.presolver)); pb_settings->set_mip_scaling(settings.mip_scaling); + pb_settings->set_symmetry(settings.symmetry); pb_settings->set_work_limit(settings.work_limit); if (settings.node_limit == std::numeric_limits::max()) { pb_settings->set_node_limit(-1); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc index a680a43df7..3e7597a536 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc @@ -15,6 +15,7 @@ settings.num_gpus = pb_settings.num_gpus(); settings.presolver = static_cast(pb_settings.presolver()); settings.mip_scaling = pb_settings.mip_scaling(); + settings.symmetry = pb_settings.symmetry(); settings.work_limit = pb_settings.work_limit(); if (pb_settings.node_limit() >= 0) { settings.node_limit = static_cast(pb_settings.node_limit()); diff --git a/cpp/src/grpc/grpc_settings_mapper.cpp b/cpp/src/grpc/grpc_settings_mapper.cpp index 5f4303a7a2..ffa98e7693 100644 --- a/cpp/src/grpc/grpc_settings_mapper.cpp +++ b/cpp/src/grpc/grpc_settings_mapper.cpp @@ -82,6 +82,12 @@ void map_proto_to_mip_settings(const cuopt::remote::MIPSolverSettings& pb_settin settings.mip_scaling = CUOPT_MIP_SCALING_ON; } } + { + // symmetry: valid range matches the local-solve binding in + // solver_settings.cu ({CUOPT_MIP_SYMMETRY, ..., -1, 2, -1}). + auto sv = pb_settings.symmetry(); + if (sv < -1 || sv > 2) { settings.symmetry = -1; } + } } // Explicit template instantiations diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index ef97f4a414..399c9a109e 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -1665,6 +1665,7 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) orig.num_gpus = 2; orig.presolver = presolver_t::Default; orig.mip_scaling = true; + orig.symmetry = 2; // orbital fixing + lexical reduction // Branching orig.reliability_branching = 32; @@ -1712,6 +1713,7 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) EXPECT_EQ(restored.num_gpus, 2); EXPECT_EQ(restored.presolver, presolver_t::Default); EXPECT_EQ(restored.mip_scaling, true); + EXPECT_EQ(restored.symmetry, 2); // Branching EXPECT_EQ(restored.reliability_branching, 32); @@ -1733,6 +1735,34 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) EXPECT_EQ(restored.seed, 12345); } +TEST(MapperRoundtrip, MIPSettingsSymmetryClampsOutOfRange) +{ + // The local-solve binding (solver_settings.cu) restricts symmetry to [-1, 2]. + // The mapper applies the same range to defend against buggy/untrusted clients. + for (int bad_value : {-2, 3, 99, std::numeric_limits::min()}) { + cuopt::remote::MIPSolverSettings pb; + pb.set_symmetry(bad_value); + + mip_solver_settings_t restored; + restored.symmetry = 0; // confirm clamp actively overwrites + map_proto_to_mip_settings(pb, restored); + + EXPECT_EQ(restored.symmetry, -1) << "symmetry=" << bad_value << " should clamp to -1 (default)"; + } + + // In-range values pass through unchanged. + for (int good_value : {-1, 0, 1, 2}) { + cuopt::remote::MIPSolverSettings pb; + pb.set_symmetry(good_value); + + mip_solver_settings_t restored; + map_proto_to_mip_settings(pb, restored); + + EXPECT_EQ(restored.symmetry, good_value) + << "symmetry=" << good_value << " should round-trip unchanged"; + } +} + TEST(MapperRoundtrip, MIPSettingsNodeLimitSentinel) { mip_solver_settings_t orig; From 93ec525c31c48278b6f17c12617f4b94c2172762 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 26 May 2026 16:05:24 -0400 Subject: [PATCH 08/15] add grpc transport for mip_settings.semi_continuous_big_m --- cpp/src/grpc/codegen/field_registry.yaml | 7 +++++++ cpp/src/grpc/codegen/generated/cuopt_remote_data.proto | 1 + .../codegen/generated/generated_mip_settings_to_proto.inc | 1 + .../codegen/generated/generated_proto_to_mip_settings.inc | 1 + cpp/tests/linear_programming/grpc/grpc_client_test.cpp | 6 ++++++ 5 files changed, 16 insertions(+) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index 18bbafed23..db4887c8dc 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -627,6 +627,13 @@ mip_settings: field_num: 29 type: bool + # Semi-continuous variables + # Fallback upper bound for semi-continuous variables when bounds-strengthening + # cannot derive a finite UB. Local-solve binding restricts the range to + # [1.0, +inf] with default 1e10; see solver_settings.cu. + - semi_continuous_big_m: + field_num: 34 + # ───────────────────────────────────────────────────────────────────────────── # Optimization Problem (cpu_optimization_problem_t) # ───────────────────────────────────────────────────────────────────────────── diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 5f53e4ee99..8b296bbe82 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -193,6 +193,7 @@ message MIPSolverSettings { int32 implied_bound_cuts = 31; int32 mip_batch_pdlp_reliability_branching = 32; int32 symmetry = 33; + double semi_continuous_big_m = 34; } message PDLPWarmStartData { diff --git a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc index 8993330a69..7d3077363a 100644 --- a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc +++ b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc @@ -39,3 +39,4 @@ pb_settings->set_determinism_mode(settings.determinism_mode); pb_settings->set_seed(settings.seed); pb_settings->set_probing(settings.probing); + pb_settings->set_semi_continuous_big_m(settings.semi_continuous_big_m); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc index 3e7597a536..3e14ec5872 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc @@ -37,3 +37,4 @@ settings.determinism_mode = pb_settings.determinism_mode(); settings.seed = pb_settings.seed(); settings.probing = pb_settings.probing(); + settings.semi_continuous_big_m = pb_settings.semi_continuous_big_m(); diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index 399c9a109e..ca96ef33a6 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -1667,6 +1667,9 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) orig.mip_scaling = true; orig.symmetry = 2; // orbital fixing + lexical reduction + // Semi-continuous variables + orig.semi_continuous_big_m = 7.5e9; // not the default 1e10, to detect overwrite-on-decode + // Branching orig.reliability_branching = 32; orig.mip_batch_pdlp_strong_branching = 16; @@ -1715,6 +1718,9 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) EXPECT_EQ(restored.mip_scaling, true); EXPECT_EQ(restored.symmetry, 2); + // Semi-continuous variables + EXPECT_DOUBLE_EQ(restored.semi_continuous_big_m, 7.5e9); + // Branching EXPECT_EQ(restored.reliability_branching, 32); EXPECT_EQ(restored.mip_batch_pdlp_strong_branching, 16); From 79909b46192ce3b72afba07a153d064c4d72f46e Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 26 May 2026 16:10:35 -0400 Subject: [PATCH 09/15] add grpc transport for barrier method settings * barrier_step_scale * barrier_iterative_refinement --- cpp/src/grpc/codegen/field_registry.yaml | 14 ++++++ .../codegen/generated/cuopt_remote_data.proto | 2 + .../generated_pdlp_settings_to_proto.inc | 2 + .../generated_proto_to_pdlp_settings.inc | 2 + .../grpc/grpc_client_test.cpp | 48 ++++++++++--------- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index db4887c8dc..0d7d681830 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -490,6 +490,20 @@ pdlp_settings: - eliminate_dense_columns: field_num: 27 type: bool + - barrier_iterative_refinement: + # Whether the barrier method runs iterative refinement after each solve + # (see cpp/src/barrier/barrier.cu). C++ default is `true`; if an old + # client (predating this field) hits a newer server, the proto3 wire + # default of `false` will overwrite the C++ default. This matches the + # pre-existing behavior of mip_settings.probing (also default-true bool). + field_num: 31 + type: bool + - barrier_step_scale: + # Step scale used by the barrier method primal/dual updates. Local-solve + # binding restricts the range to [0.5, 0.9999] with default 0.9; see + # solver_settings.cu. No post-decode clamp is applied here for + # consistency with the other f_t scalars (tolerances, time_limit, ...). + field_num: 32 - save_best_primal_so_far: field_num: 28 type: bool diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 8b296bbe82..5839019199 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -156,6 +156,8 @@ message PDLPSolverSettings { bool save_best_primal_so_far = 28; bool first_primal_feasible = 29; int32 pdlp_precision = 30; + bool barrier_iterative_refinement = 31; + double barrier_step_scale = 32; PDLPWarmStartData warm_start_data = 50; } diff --git a/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc index fbd863fd6d..eb00a500ad 100644 --- a/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc +++ b/cpp/src/grpc/codegen/generated/generated_pdlp_settings_to_proto.inc @@ -33,6 +33,8 @@ pb_settings->set_ordering(settings.ordering); pb_settings->set_barrier_dual_initial_point(settings.barrier_dual_initial_point); pb_settings->set_eliminate_dense_columns(settings.eliminate_dense_columns); + pb_settings->set_barrier_iterative_refinement(settings.barrier_iterative_refinement); + pb_settings->set_barrier_step_scale(settings.barrier_step_scale); pb_settings->set_save_best_primal_so_far(settings.save_best_primal_so_far); pb_settings->set_first_primal_feasible(settings.first_primal_feasible); pb_settings->set_pdlp_precision(static_cast(settings.pdlp_precision)); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc index 7f1de3bbd2..b5d978aabb 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc @@ -31,6 +31,8 @@ settings.ordering = pb_settings.ordering(); settings.barrier_dual_initial_point = pb_settings.barrier_dual_initial_point(); settings.eliminate_dense_columns = pb_settings.eliminate_dense_columns(); + settings.barrier_iterative_refinement = pb_settings.barrier_iterative_refinement(); + settings.barrier_step_scale = pb_settings.barrier_step_scale(); settings.save_best_primal_so_far = pb_settings.save_best_primal_so_far(); settings.first_primal_feasible = pb_settings.first_primal_feasible(); settings.pdlp_precision = static_cast(pb_settings.pdlp_precision()); diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index ca96ef33a6..bf97e2f322 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -1940,28 +1940,30 @@ TEST(MapperRoundtrip, PDLPSettingsAllFields) orig.tolerances.absolute_primal_tolerance = 5e-7; orig.tolerances.relative_primal_tolerance = 6e-7; - orig.time_limit = 99.5; - orig.iteration_limit = 10000; - orig.log_to_console = false; - orig.detect_infeasibility = true; - orig.strict_infeasibility = true; - orig.pdlp_solver_mode = pdlp_solver_mode_t::Fast1; - orig.method = method_t::Barrier; - orig.presolver = presolver_t::Default; - orig.dual_postsolve = true; - orig.crossover = true; - orig.num_gpus = 4; - orig.per_constraint_residual = true; - orig.cudss_deterministic = true; - orig.folding = 1; - orig.augmented = 1; - orig.dualize = 1; - orig.ordering = 2; - orig.barrier_dual_initial_point = 1; - orig.eliminate_dense_columns = true; - orig.pdlp_precision = pdlp_precision_t::MixedPrecision; - orig.save_best_primal_so_far = true; - orig.first_primal_feasible = true; + orig.time_limit = 99.5; + orig.iteration_limit = 10000; + orig.log_to_console = false; + orig.detect_infeasibility = true; + orig.strict_infeasibility = true; + orig.pdlp_solver_mode = pdlp_solver_mode_t::Fast1; + orig.method = method_t::Barrier; + orig.presolver = presolver_t::Default; + orig.dual_postsolve = true; + orig.crossover = true; + orig.num_gpus = 4; + orig.per_constraint_residual = true; + orig.cudss_deterministic = true; + orig.folding = 1; + orig.augmented = 1; + orig.dualize = 1; + orig.ordering = 2; + orig.barrier_dual_initial_point = 1; + orig.eliminate_dense_columns = true; + orig.barrier_iterative_refinement = false; // not the default true, to detect overwrite-on-decode + orig.barrier_step_scale = 0.75; // not the default 0.9 + orig.pdlp_precision = pdlp_precision_t::MixedPrecision; + orig.save_best_primal_so_far = true; + orig.first_primal_feasible = true; cuopt::remote::PDLPSolverSettings pb; map_pdlp_settings_to_proto(orig, &pb); @@ -1997,6 +1999,8 @@ TEST(MapperRoundtrip, PDLPSettingsAllFields) EXPECT_EQ(restored.ordering, 2); EXPECT_EQ(restored.barrier_dual_initial_point, 1); EXPECT_EQ(restored.eliminate_dense_columns, true); + EXPECT_EQ(restored.barrier_iterative_refinement, false); + EXPECT_DOUBLE_EQ(restored.barrier_step_scale, 0.75); EXPECT_EQ(restored.pdlp_precision, pdlp_precision_t::MixedPrecision); EXPECT_EQ(restored.save_best_primal_so_far, true); EXPECT_EQ(restored.first_primal_feasible, true); From 62a2bf53ab14790171afe2bc1f40e9ca25bb7d46 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 26 May 2026 16:16:56 -0400 Subject: [PATCH 10/15] add grpc transport for mip heuristics parameters --- cpp/src/grpc/codegen/field_registry.yaml | 51 +++++++++++++++++++ .../codegen/generated/cuopt_remote_data.proto | 17 +++++++ .../generated_mip_settings_to_proto.inc | 17 +++++++ .../generated_proto_to_mip_settings.inc | 17 +++++++ .../grpc/grpc_client_test.cpp | 40 +++++++++++++++ 5 files changed, 142 insertions(+) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index 0d7d681830..f77d73a318 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -648,6 +648,57 @@ mip_settings: - semi_continuous_big_m: field_num: 34 + # MIP heuristic hyper-parameters (nested: settings.heuristic_params.) + # Tuning knobs for MIP GPU heuristics; see mip_heuristics_hyper_params_t + # in cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp. + # All fields carry non-zero defaults on the C++ side, so an old client + # predating any of these fields will silently revert the corresponding + # value to the proto3 wire default (0) on the server. This matches the + # pre-existing behavior of mip_settings.probing / barrier settings, and + # is the price of using bare scalars in proto3 (no field-presence). + - heuristic_params: + - population_size: + field_num: 35 + type: int32 + - num_cpufj_threads: + field_num: 36 + type: int32 + - presolve_time_ratio: + field_num: 37 + - presolve_max_time: + field_num: 38 + - root_lp_time_ratio: + field_num: 39 + - root_lp_max_time: + field_num: 40 + - rins_time_limit: + field_num: 41 + - rins_max_time_limit: + field_num: 42 + - rins_fix_rate: + field_num: 43 + - stagnation_trigger: + field_num: 44 + type: int32 + - max_iterations_without_improvement: + field_num: 45 + type: int32 + - initial_infeasibility_weight: + field_num: 46 + - n_of_minimums_for_exit: + field_num: 47 + type: int32 + - enabled_recombiners: + field_num: 48 + type: int32 + - cycle_detection_length: + field_num: 49 + type: int32 + - relaxed_lp_time_limit: + field_num: 50 + - related_vars_time_limit: + field_num: 51 + # ───────────────────────────────────────────────────────────────────────────── # Optimization Problem (cpu_optimization_problem_t) # ───────────────────────────────────────────────────────────────────────────── diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 5839019199..9f118363ea 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -196,6 +196,23 @@ message MIPSolverSettings { int32 mip_batch_pdlp_reliability_branching = 32; int32 symmetry = 33; double semi_continuous_big_m = 34; + int32 population_size = 35; + int32 num_cpufj_threads = 36; + double presolve_time_ratio = 37; + double presolve_max_time = 38; + double root_lp_time_ratio = 39; + double root_lp_max_time = 40; + double rins_time_limit = 41; + double rins_max_time_limit = 42; + double rins_fix_rate = 43; + int32 stagnation_trigger = 44; + int32 max_iterations_without_improvement = 45; + double initial_infeasibility_weight = 46; + int32 n_of_minimums_for_exit = 47; + int32 enabled_recombiners = 48; + int32 cycle_detection_length = 49; + double relaxed_lp_time_limit = 50; + double related_vars_time_limit = 51; } message PDLPWarmStartData { diff --git a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc index 7d3077363a..aeabff36fc 100644 --- a/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc +++ b/cpp/src/grpc/codegen/generated/generated_mip_settings_to_proto.inc @@ -40,3 +40,20 @@ pb_settings->set_seed(settings.seed); pb_settings->set_probing(settings.probing); pb_settings->set_semi_continuous_big_m(settings.semi_continuous_big_m); + pb_settings->set_population_size(settings.heuristic_params.population_size); + pb_settings->set_num_cpufj_threads(settings.heuristic_params.num_cpufj_threads); + pb_settings->set_presolve_time_ratio(settings.heuristic_params.presolve_time_ratio); + pb_settings->set_presolve_max_time(settings.heuristic_params.presolve_max_time); + pb_settings->set_root_lp_time_ratio(settings.heuristic_params.root_lp_time_ratio); + pb_settings->set_root_lp_max_time(settings.heuristic_params.root_lp_max_time); + pb_settings->set_rins_time_limit(settings.heuristic_params.rins_time_limit); + pb_settings->set_rins_max_time_limit(settings.heuristic_params.rins_max_time_limit); + pb_settings->set_rins_fix_rate(settings.heuristic_params.rins_fix_rate); + pb_settings->set_stagnation_trigger(settings.heuristic_params.stagnation_trigger); + pb_settings->set_max_iterations_without_improvement(settings.heuristic_params.max_iterations_without_improvement); + pb_settings->set_initial_infeasibility_weight(settings.heuristic_params.initial_infeasibility_weight); + pb_settings->set_n_of_minimums_for_exit(settings.heuristic_params.n_of_minimums_for_exit); + pb_settings->set_enabled_recombiners(settings.heuristic_params.enabled_recombiners); + pb_settings->set_cycle_detection_length(settings.heuristic_params.cycle_detection_length); + pb_settings->set_relaxed_lp_time_limit(settings.heuristic_params.relaxed_lp_time_limit); + pb_settings->set_related_vars_time_limit(settings.heuristic_params.related_vars_time_limit); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc index 3e14ec5872..ef3c11dbfa 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc @@ -38,3 +38,20 @@ settings.seed = pb_settings.seed(); settings.probing = pb_settings.probing(); settings.semi_continuous_big_m = pb_settings.semi_continuous_big_m(); + settings.heuristic_params.population_size = pb_settings.population_size(); + settings.heuristic_params.num_cpufj_threads = pb_settings.num_cpufj_threads(); + settings.heuristic_params.presolve_time_ratio = pb_settings.presolve_time_ratio(); + settings.heuristic_params.presolve_max_time = pb_settings.presolve_max_time(); + settings.heuristic_params.root_lp_time_ratio = pb_settings.root_lp_time_ratio(); + settings.heuristic_params.root_lp_max_time = pb_settings.root_lp_max_time(); + settings.heuristic_params.rins_time_limit = pb_settings.rins_time_limit(); + settings.heuristic_params.rins_max_time_limit = pb_settings.rins_max_time_limit(); + settings.heuristic_params.rins_fix_rate = pb_settings.rins_fix_rate(); + settings.heuristic_params.stagnation_trigger = pb_settings.stagnation_trigger(); + settings.heuristic_params.max_iterations_without_improvement = pb_settings.max_iterations_without_improvement(); + settings.heuristic_params.initial_infeasibility_weight = pb_settings.initial_infeasibility_weight(); + settings.heuristic_params.n_of_minimums_for_exit = pb_settings.n_of_minimums_for_exit(); + settings.heuristic_params.enabled_recombiners = pb_settings.enabled_recombiners(); + settings.heuristic_params.cycle_detection_length = pb_settings.cycle_detection_length(); + settings.heuristic_params.relaxed_lp_time_limit = pb_settings.relaxed_lp_time_limit(); + settings.heuristic_params.related_vars_time_limit = pb_settings.related_vars_time_limit(); diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index bf97e2f322..9bb479d62e 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -1689,6 +1689,27 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) orig.determinism_mode = CUOPT_MODE_DETERMINISTIC; orig.seed = 12345; + // Heuristic hyper-parameters (mip_heuristics_hyper_params_t). + // Set every field to a value distinct from its C++ default so a missed + // mapping line would produce a default-valued mismatch on decode. + orig.heuristic_params.population_size = 64; // default 32 + orig.heuristic_params.num_cpufj_threads = 4; // default 8 + orig.heuristic_params.presolve_time_ratio = 0.2; // default 0.1 + orig.heuristic_params.presolve_max_time = 45.0; // default 60.0 + orig.heuristic_params.root_lp_time_ratio = 0.25; // default 0.1 + orig.heuristic_params.root_lp_max_time = 7.5; // default 15.0 + orig.heuristic_params.rins_time_limit = 4.0; // default 3.0 + orig.heuristic_params.rins_max_time_limit = 25.0; // default 20.0 + orig.heuristic_params.rins_fix_rate = 0.75; // default 0.5 + orig.heuristic_params.stagnation_trigger = 5; // default 3 + orig.heuristic_params.max_iterations_without_improvement = 12; // default 8 + orig.heuristic_params.initial_infeasibility_weight = 500.0; // default 1000.0 + orig.heuristic_params.n_of_minimums_for_exit = 9000; // default 7000 + orig.heuristic_params.enabled_recombiners = 7; // default 15 (bitmask) + orig.heuristic_params.cycle_detection_length = 40; // default 30 + orig.heuristic_params.relaxed_lp_time_limit = 2.5; // default 1.0 + orig.heuristic_params.related_vars_time_limit = 45.0; // default 30.0 + // Roundtrip: C++ -> proto -> C++ cuopt::remote::MIPSolverSettings pb; map_mip_settings_to_proto(orig, &pb); @@ -1739,6 +1760,25 @@ TEST(MapperRoundtrip, MIPSettingsAllFields) // Determinism and reproducibility EXPECT_EQ(restored.determinism_mode, CUOPT_MODE_DETERMINISTIC); EXPECT_EQ(restored.seed, 12345); + + // Heuristic hyper-parameters + EXPECT_EQ(restored.heuristic_params.population_size, 64); + EXPECT_EQ(restored.heuristic_params.num_cpufj_threads, 4); + EXPECT_DOUBLE_EQ(restored.heuristic_params.presolve_time_ratio, 0.2); + EXPECT_DOUBLE_EQ(restored.heuristic_params.presolve_max_time, 45.0); + EXPECT_DOUBLE_EQ(restored.heuristic_params.root_lp_time_ratio, 0.25); + EXPECT_DOUBLE_EQ(restored.heuristic_params.root_lp_max_time, 7.5); + EXPECT_DOUBLE_EQ(restored.heuristic_params.rins_time_limit, 4.0); + EXPECT_DOUBLE_EQ(restored.heuristic_params.rins_max_time_limit, 25.0); + EXPECT_DOUBLE_EQ(restored.heuristic_params.rins_fix_rate, 0.75); + EXPECT_EQ(restored.heuristic_params.stagnation_trigger, 5); + EXPECT_EQ(restored.heuristic_params.max_iterations_without_improvement, 12); + EXPECT_DOUBLE_EQ(restored.heuristic_params.initial_infeasibility_weight, 500.0); + EXPECT_EQ(restored.heuristic_params.n_of_minimums_for_exit, 9000); + EXPECT_EQ(restored.heuristic_params.enabled_recombiners, 7); + EXPECT_EQ(restored.heuristic_params.cycle_detection_length, 40); + EXPECT_DOUBLE_EQ(restored.heuristic_params.relaxed_lp_time_limit, 2.5); + EXPECT_DOUBLE_EQ(restored.heuristic_params.related_vars_time_limit, 45.0); } TEST(MapperRoundtrip, MIPSettingsSymmetryClampsOutOfRange) From eca6f9e9cc23567a21f21b817738c349301ae79e Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Tue, 26 May 2026 16:54:47 -0400 Subject: [PATCH 11/15] move grpc developer docs into cpp/src/grpc --- {docs/cuopt => cpp/src}/grpc/GRPC_CODE_GENERATION.md | 0 {docs/cuopt => cpp/src}/grpc/GRPC_INTERFACE.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {docs/cuopt => cpp/src}/grpc/GRPC_CODE_GENERATION.md (100%) rename {docs/cuopt => cpp/src}/grpc/GRPC_INTERFACE.md (100%) diff --git a/docs/cuopt/grpc/GRPC_CODE_GENERATION.md b/cpp/src/grpc/GRPC_CODE_GENERATION.md similarity index 100% rename from docs/cuopt/grpc/GRPC_CODE_GENERATION.md rename to cpp/src/grpc/GRPC_CODE_GENERATION.md diff --git a/docs/cuopt/grpc/GRPC_INTERFACE.md b/cpp/src/grpc/GRPC_INTERFACE.md similarity index 100% rename from docs/cuopt/grpc/GRPC_INTERFACE.md rename to cpp/src/grpc/GRPC_INTERFACE.md From 28bb4825e93970bb081fda3f063ac2439e0d72c1 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 27 May 2026 16:24:45 -0400 Subject: [PATCH 12/15] grpc/codegen: add `optional` presence for default-true scalar settings in proto3 without the "optional" keyword there is no way to distinguish between "explicitly false" and omitted fields (which proto3 sets to false). This is a problem for scalars with a default True value in C++. Marking those fields optionals allows the server to determine explicit false vs omitted and react accordingly. --- cpp/src/grpc/codegen/field_registry.yaml | 27 +++++- cpp/src/grpc/codegen/generate_conversions.py | 70 +++++++++++++--- .../codegen/generated/cuopt_remote_data.proto | 6 +- .../generated_proto_to_mip_settings.inc | 4 +- .../generated_proto_to_pdlp_settings.inc | 8 +- .../grpc/grpc_client_test.cpp | 82 +++++++++++++++++++ 6 files changed, 175 insertions(+), 22 deletions(-) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index f77d73a318..b780e30b69 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -50,6 +50,13 @@ # skip_conversion – true => include in proto but not in conversion code # proto_only – true => include in proto header but not a constructor arg # sentinel – sentinel value handling for fields with "unset" semantics +# optional – true => emit proto3 `optional` (presence-tracked) and +# only apply the value when the client explicitly set it. +# Use for scalar fields whose C++ default differs from the +# proto3 zero value (e.g. `bool foo{true}`), so that an +# omitted field preserves the solver default instead of +# silently overwriting it with the proto3 zero. Mutually +# exclusive with `sentinel`. Not supported on enum types. # to_proto_cast – explicit cast type for C++ -> proto direction # from_proto_cast – explicit cast type for proto -> C++ direction # @@ -457,8 +464,13 @@ pdlp_settings: to_proto_cast: "int32_t" from_proto_cast: "presolver_t" - dual_postsolve: + # C++ default is `true`; using proto3 `optional` so that a client that + # omits this field gets the solver default rather than the proto3 zero + # (`false`). See also the matching `optional` markers on + # `barrier_iterative_refinement` (pdlp) and `probing` (mip). field_num: 17 type: bool + optional: true - crossover: field_num: 18 type: bool @@ -492,12 +504,13 @@ pdlp_settings: type: bool - barrier_iterative_refinement: # Whether the barrier method runs iterative refinement after each solve - # (see cpp/src/barrier/barrier.cu). C++ default is `true`; if an old - # client (predating this field) hits a newer server, the proto3 wire - # default of `false` will overwrite the C++ default. This matches the - # pre-existing behavior of mip_settings.probing (also default-true bool). + # (see cpp/src/barrier/barrier.cu). C++ default is `true`. Declared as + # proto3 `optional` so that a client which omits this field preserves + # the solver default; without `optional`, the proto3 wire zero (`false`) + # would silently overwrite the C++ default. field_num: 31 type: bool + optional: true - barrier_step_scale: # Step scale used by the barrier method primal/dual updates. Local-solve # binding restricts the range to [0.5, 0.9999] with default 0.9; see @@ -638,8 +651,14 @@ mip_settings: # Presolve sub-steps - probing: + # C++ default is `true`; declared as proto3 `optional` so that a client + # which omits this field preserves the solver default. Without + # `optional`, the proto3 wire zero (`false`) would silently overwrite + # the C++ default. See also `dual_postsolve` and + # `barrier_iterative_refinement` in pdlp_settings. field_num: 29 type: bool + optional: true # Semi-continuous variables # Fallback upper bound for semi-continuous variables when bounds-strengthening diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index d56b020a41..1ec6cd992b 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -576,7 +576,8 @@ def generate_settings_message_proto(registry, message_name, obj): if num is None: continue ptype = _settings_field_proto_type(registry, f) - lines.append((num, f" {ptype} {f['name']} = {num};")) + prefix = "optional " if f.get("optional") else "" + lines.append((num, f" {prefix}{ptype} {f['name']} = {num};")) lines.extend(_iter_embeds(obj)) lines.sort(key=lambda x: x[0]) return "\n".join(item[1] for item in lines) @@ -630,11 +631,15 @@ def generate_proto_to_settings_body(registry, obj_name, obj, indent=" "): cpp_member = f.get("member", f["name"]) from_proto_cast = f.get("from_proto_cast") sentinel = f.get("sentinel") + is_optional = bool(f.get("optional")) edef = _lookup_enum(registry, ftype) from_fn = _enum_from_proto_fn(ftype, edef) if edef else None if sentinel: + # sentinel and optional are mutually exclusive presence mechanisms; + # the validator rejects the combination. Sentinels keep their own + # value-based guard regardless of the optional flag. guard = sentinel["from_proto_guard"] cast = sentinel.get("from_proto_cast", from_proto_cast) lines.append(f"{ind}if (pb_settings.{pname}() {guard}) {{") @@ -645,18 +650,23 @@ def generate_proto_to_settings_body(registry, obj_name, obj, indent=" "): ) lines.append(f"{ind} settings.{cpp_member} = {expr};") lines.append(f"{ind}}}") - elif from_fn: - lines.append( - f"{ind}settings.{cpp_member} = {from_fn}(pb_settings.{pname}());" - ) - elif from_proto_cast: - lines.append( - f"{ind}settings.{cpp_member} = static_cast<{from_proto_cast}>(pb_settings.{pname}());" - ) else: - lines.append( - f"{ind}settings.{cpp_member} = pb_settings.{pname}();" - ) + if from_fn: + rhs = f"{from_fn}(pb_settings.{pname}())" + elif from_proto_cast: + rhs = f"static_cast<{from_proto_cast}>(pb_settings.{pname}())" + else: + rhs = f"pb_settings.{pname}()" + + if is_optional: + # `optional` proto3 scalar: only apply the value when the + # client explicitly set it on the wire, so omitted fields + # preserve the C++ struct's in-class default. + lines.append(f"{ind}if (pb_settings.has_{pname}()) {{") + lines.append(f"{ind} settings.{cpp_member} = {rhs};") + lines.append(f"{ind}}}") + else: + lines.append(f"{ind}settings.{cpp_member} = {rhs};") return "\n".join(lines) @@ -2146,6 +2156,42 @@ def _validate_registry_uniqueness(registry): ] _check_unique("ArrayFieldId", afid_pairs, errors) + # `optional: true` is only meaningful for scalar proto3 fields that have + # a settable C++ default different from the proto3 zero value. The codegen + # only emits `optional ` + a `has_()` guard for the scalar settings + # path; we reject combinations that would silently do the wrong thing. + # + # Specifically: + # * Sentinel fields already encode "unset" via a reserved value and the + # mapper uses `from_proto_guard` to recover the C++ default — pairing + # them with `optional` is redundant and the two presence mechanisms + # would fight (`has_X()==true && value==sentinel` is ambiguous). + # * Enum-typed fields are syntactically allowed by proto3 but our enums + # follow the "UNSPECIFIED = 0 means default" convention; flipping them + # to `optional` would change semantics in ways we want to think about + # case-by-case, not implicitly. + for msg, key in [ + ("PDLPSolverSettings", "pdlp_settings"), + ("MIPSolverSettings", "mip_settings"), + ]: + section = registry.get(key) or {} + for f in parse_settings_fields(section.get("fields", [])): + if not f.get("optional"): + continue + name = f.get("name") + if f.get("sentinel"): + errors.append( + f"{msg}.{name}: `optional` and `sentinel` are mutually " + "exclusive presence mechanisms" + ) + ftype = f.get("type", "double") + if _lookup_enum(registry, ftype): + errors.append( + f"{msg}.{name}: `optional` is not supported on enum-typed " + f"fields (type {ftype!r}); enums use the UNSPECIFIED=0 " + "convention for defaults" + ) + if errors: raise ValueError( "field_registry.yaml validation failed:\n - " diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 9f118363ea..9933210df0 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -142,7 +142,7 @@ message PDLPSolverSettings { PDLPSolverMode pdlp_solver_mode = 14; LPMethod method = 15; int32 presolver = 16; - bool dual_postsolve = 17; + optional bool dual_postsolve = 17; bool crossover = 18; int32 num_gpus = 19; bool per_constraint_residual = 20; @@ -156,7 +156,7 @@ message PDLPSolverSettings { bool save_best_primal_so_far = 28; bool first_primal_feasible = 29; int32 pdlp_precision = 30; - bool barrier_iterative_refinement = 31; + optional bool barrier_iterative_refinement = 31; double barrier_step_scale = 32; PDLPWarmStartData warm_start_data = 50; } @@ -190,7 +190,7 @@ message MIPSolverSettings { double cut_min_orthogonality = 26; int32 determinism_mode = 27; int32 seed = 28; - bool probing = 29; + optional bool probing = 29; int32 strong_branching_simplex_iteration_limit = 30; int32 implied_bound_cuts = 31; int32 mip_batch_pdlp_reliability_branching = 32; diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc index ef3c11dbfa..bcc48d2660 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc @@ -36,7 +36,9 @@ settings.cut_min_orthogonality = pb_settings.cut_min_orthogonality(); settings.determinism_mode = pb_settings.determinism_mode(); settings.seed = pb_settings.seed(); - settings.probing = pb_settings.probing(); + if (pb_settings.has_probing()) { + settings.probing = pb_settings.probing(); + } settings.semi_continuous_big_m = pb_settings.semi_continuous_big_m(); settings.heuristic_params.population_size = pb_settings.population_size(); settings.heuristic_params.num_cpufj_threads = pb_settings.num_cpufj_threads(); diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc index b5d978aabb..446b58ab94 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc @@ -20,7 +20,9 @@ settings.pdlp_solver_mode = from_proto_pdlp_solver_mode(pb_settings.pdlp_solver_mode()); settings.method = from_proto_lp_method(pb_settings.method()); settings.presolver = static_cast(pb_settings.presolver()); - settings.dual_postsolve = pb_settings.dual_postsolve(); + if (pb_settings.has_dual_postsolve()) { + settings.dual_postsolve = pb_settings.dual_postsolve(); + } settings.crossover = pb_settings.crossover(); settings.num_gpus = pb_settings.num_gpus(); settings.per_constraint_residual = pb_settings.per_constraint_residual(); @@ -31,7 +33,9 @@ settings.ordering = pb_settings.ordering(); settings.barrier_dual_initial_point = pb_settings.barrier_dual_initial_point(); settings.eliminate_dense_columns = pb_settings.eliminate_dense_columns(); - settings.barrier_iterative_refinement = pb_settings.barrier_iterative_refinement(); + if (pb_settings.has_barrier_iterative_refinement()) { + settings.barrier_iterative_refinement = pb_settings.barrier_iterative_refinement(); + } settings.barrier_step_scale = pb_settings.barrier_step_scale(); settings.save_best_primal_so_far = pb_settings.save_best_primal_so_far(); settings.first_primal_feasible = pb_settings.first_primal_feasible(); diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index 9bb479d62e..dedd055430 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -2060,3 +2060,85 @@ TEST(MapperRoundtrip, PDLPSettingsIterationLimitSentinel) map_proto_to_pdlp_settings(pb, restored); EXPECT_EQ(restored.iteration_limit, default_limit) << "Negative sentinel should keep default"; } + +// ─────────────────────────────────────────────────────────────────────────── +// Proto3 `optional` presence handling +// ─────────────────────────────────────────────────────────────────────────── +// +// A handful of bool settings have a C++ default of `true` but live on the wire +// in a proto3 message. Without `optional`, an omitted field decodes as the +// proto3 zero (`false`) and the mapper would silently overwrite the C++ +// default. The codegen now emits `optional ` for these fields and +// guards the assignment with `has_()`, so an omitted field preserves the +// solver default. The tests below pin that behavior for the three currently +// converted fields. +TEST(MapperRoundtrip, MIPSettingsProbingOmittedPreservesDefault) +{ + cuopt::remote::MIPSolverSettings pb; // default-constructed: probing absent + + mip_solver_settings_t restored; + ASSERT_TRUE(restored.probing) << "C++ default is expected to be true"; + restored.probing = false; // confirm the guard actively skips the assignment + map_proto_to_mip_settings(pb, restored); + EXPECT_FALSE(restored.probing) + << "Omitted optional bool must not overwrite the existing struct value; " + "the in-class default would be restored only if the struct was fresh"; + + mip_solver_settings_t fresh; + map_proto_to_mip_settings(pb, fresh); + EXPECT_TRUE(fresh.probing) << "Omitted optional bool must preserve the C++ default `true`"; +} + +TEST(MapperRoundtrip, MIPSettingsProbingExplicitFalseRoundtrips) +{ + cuopt::remote::MIPSolverSettings pb; + pb.set_probing(false); + ASSERT_TRUE(pb.has_probing()) << "set_probing must mark presence on optional field"; + + mip_solver_settings_t restored; + map_proto_to_mip_settings(pb, restored); + EXPECT_FALSE(restored.probing) << "Explicit false must apply"; +} + +TEST(MapperRoundtrip, PDLPSettingsDualPostsolveOmittedPreservesDefault) +{ + cuopt::remote::PDLPSolverSettings pb; // default-constructed + + pdlp_solver_settings_t fresh; + ASSERT_TRUE(fresh.dual_postsolve) << "C++ default is expected to be true"; + map_proto_to_pdlp_settings(pb, fresh); + EXPECT_TRUE(fresh.dual_postsolve) << "Omitted optional bool must preserve the C++ default `true`"; +} + +TEST(MapperRoundtrip, PDLPSettingsDualPostsolveExplicitFalseRoundtrips) +{ + cuopt::remote::PDLPSolverSettings pb; + pb.set_dual_postsolve(false); + ASSERT_TRUE(pb.has_dual_postsolve()); + + pdlp_solver_settings_t restored; + map_proto_to_pdlp_settings(pb, restored); + EXPECT_FALSE(restored.dual_postsolve); +} + +TEST(MapperRoundtrip, PDLPSettingsBarrierIterativeRefinementOmittedPreservesDefault) +{ + cuopt::remote::PDLPSolverSettings pb; + + pdlp_solver_settings_t fresh; + ASSERT_TRUE(fresh.barrier_iterative_refinement); + map_proto_to_pdlp_settings(pb, fresh); + EXPECT_TRUE(fresh.barrier_iterative_refinement) + << "Omitted optional bool must preserve the C++ default `true`"; +} + +TEST(MapperRoundtrip, PDLPSettingsBarrierIterativeRefinementExplicitFalseRoundtrips) +{ + cuopt::remote::PDLPSolverSettings pb; + pb.set_barrier_iterative_refinement(false); + ASSERT_TRUE(pb.has_barrier_iterative_refinement()); + + pdlp_solver_settings_t restored; + map_proto_to_pdlp_settings(pb, restored); + EXPECT_FALSE(restored.barrier_iterative_refinement); +} From 7faedd45dbebe0441c7eedad6e8ed327c6487eaa Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 27 May 2026 17:05:28 -0400 Subject: [PATCH 13/15] grpc/codegen: extend `optional` presence to all non-zero-default scalars same case as for bool settings with True C++ defaults. Scalars with non-zero default values need to be marked 'optional' so the server can distinguish 'explicit 0' from 'omitted'. --- cpp/src/grpc/codegen/field_registry.yaml | 89 ++++++++- cpp/src/grpc/codegen/generate_conversions.py | 64 +++--- .../codegen/generated/cuopt_remote_data.proto | 134 ++++++------- .../generated_proto_to_mip_settings.inc | 188 +++++++++++++----- .../generated_proto_to_pdlp_settings.inc | 88 +++++--- .../grpc/grpc_client_test.cpp | 110 ++++++++++ 6 files changed, 495 insertions(+), 178 deletions(-) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index b780e30b69..ceaf4b8d70 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -55,8 +55,10 @@ # Use for scalar fields whose C++ default differs from the # proto3 zero value (e.g. `bool foo{true}`), so that an # omitted field preserves the solver default instead of -# silently overwriting it with the proto3 zero. Mutually -# exclusive with `sentinel`. Not supported on enum types. +# silently overwriting it with the proto3 zero. Composes +# with `sentinel` (optional guards the omitted case; +# sentinel keeps the existing wire encoding for "explicit +# default" values). Not supported on enum types. # to_proto_cast – explicit cast type for C++ -> proto direction # from_proto_cast – explicit cast type for proto -> C++ direction # @@ -415,25 +417,39 @@ pdlp_settings: - tolerances: - absolute_gap_tolerance: field_num: 1 + optional: true - relative_gap_tolerance: field_num: 2 + optional: true - primal_infeasible_tolerance: field_num: 3 + optional: true - dual_infeasible_tolerance: field_num: 4 + optional: true - absolute_dual_tolerance: field_num: 5 + optional: true - relative_dual_tolerance: field_num: 6 + optional: true - absolute_primal_tolerance: field_num: 7 + optional: true - relative_primal_tolerance: field_num: 8 + optional: true # Limits - time_limit: field_num: 9 + optional: true - iteration_limit: + # C++ default is `std::numeric_limits::max()`. Two presence + # mechanisms compose here: `sentinel` keeps the existing wire encoding + # (max() <=> -1, preserving 26.04 compatibility) and `optional` adds the + # missing piece — an omitted field would otherwise decode to the wire + # zero (0), which is `>= 0` and would overwrite the C++ default with 0. field_num: 10 type: int64 sentinel: @@ -441,11 +457,13 @@ pdlp_settings: proto_value: -1 from_proto_guard: ">= 0" from_proto_cast: "i_t" + optional: true # Solver configuration - log_to_console: field_num: 11 type: bool + optional: true - detect_infeasibility: field_num: 12 type: bool @@ -463,6 +481,7 @@ pdlp_settings: type: int32 to_proto_cast: "int32_t" from_proto_cast: "presolver_t" + optional: true - dual_postsolve: # C++ default is `true`; using proto3 `optional` so that a client that # omits this field gets the solver default rather than the proto3 zero @@ -477,6 +496,7 @@ pdlp_settings: - num_gpus: field_num: 19 type: int32 + optional: true - per_constraint_residual: field_num: 20 @@ -487,21 +507,27 @@ pdlp_settings: - folding: field_num: 22 type: int32 + optional: true - augmented: field_num: 23 type: int32 + optional: true - dualize: field_num: 24 type: int32 + optional: true - ordering: field_num: 25 type: int32 + optional: true - barrier_dual_initial_point: field_num: 26 type: int32 + optional: true - eliminate_dense_columns: field_num: 27 type: bool + optional: true - barrier_iterative_refinement: # Whether the barrier method runs iterative refinement after each solve # (see cpp/src/barrier/barrier.cu). C++ default is `true`. Declared as @@ -517,6 +543,7 @@ pdlp_settings: # solver_settings.cu. No post-decode clamp is applied here for # consistency with the other f_t scalars (tolerances, time_limit, ...). field_num: 32 + optional: true - save_best_primal_so_far: field_num: 28 type: bool @@ -528,6 +555,7 @@ pdlp_settings: type: int32 to_proto_cast: "int32_t" from_proto_cast: "pdlp_precision_t" + optional: true # ───────────────────────────────────────────────────────────────────────────── # MIP Solver Settings @@ -540,43 +568,55 @@ mip_settings: # Limits - time_limit: field_num: 1 + optional: true # Tolerances (nested: settings.tolerances.) - tolerances: - relative_mip_gap: field_num: 2 + optional: true - absolute_mip_gap: field_num: 3 + optional: true - integrality_tolerance: field_num: 4 + optional: true - absolute_tolerance: field_num: 5 + optional: true - relative_tolerance: field_num: 6 + optional: true - presolve_absolute_tolerance: field_num: 7 + optional: true # Solver configuration - log_to_console: field_num: 8 type: bool + optional: true - heuristics_only: field_num: 9 type: bool - num_cpu_threads: field_num: 10 type: int32 + optional: true - num_gpus: field_num: 11 type: int32 + optional: true - presolver: field_num: 12 type: int32 to_proto_cast: "int32_t" from_proto_cast: "presolver_t" + optional: true - mip_scaling: field_num: 13 type: int32 + optional: true - symmetry: # Symmetry-detection level (dejavu). Valid: -1 (default), 0 (off), # 1 (orbital fixing), 2 (orbital fixing + lexical reduction). The @@ -584,11 +624,16 @@ mip_settings: # range check in cpp/src/math_optimization/solver_settings.cu. field_num: 33 type: int32 + optional: true # Additional limits - work_limit: field_num: 14 + optional: true - node_limit: + # C++ default `max()`. See iteration_limit for the rationale on + # composing `sentinel` with `optional` (sentinel for the explicit + # wire-encoded "default" value, optional for the omitted-field case). field_num: 15 type: int32 sentinel: @@ -596,11 +641,13 @@ mip_settings: proto_value: -1 from_proto_guard: ">= 0" from_proto_cast: "i_t" + optional: true # Branching - reliability_branching: field_num: 16 type: int32 + optional: true - mip_batch_pdlp_strong_branching: field_num: 17 type: int32 @@ -610,36 +657,47 @@ mip_settings: - strong_branching_simplex_iteration_limit: field_num: 30 type: int32 + optional: true # Cut configuration - max_cut_passes: field_num: 18 type: int32 + optional: true - mir_cuts: field_num: 19 type: int32 + optional: true - mixed_integer_gomory_cuts: field_num: 20 type: int32 + optional: true - knapsack_cuts: field_num: 21 type: int32 + optional: true - clique_cuts: field_num: 22 type: int32 + optional: true - implied_bound_cuts: field_num: 31 type: int32 + optional: true - strong_chvatal_gomory_cuts: field_num: 23 type: int32 + optional: true - reduced_cost_strengthening: field_num: 24 type: int32 + optional: true - cut_change_threshold: field_num: 25 + optional: true - cut_min_orthogonality: field_num: 26 + optional: true # Determinism and reproducibility - determinism_mode: @@ -648,6 +706,7 @@ mip_settings: - seed: field_num: 28 type: int32 + optional: true # Presolve sub-steps - probing: @@ -666,57 +725,73 @@ mip_settings: # [1.0, +inf] with default 1e10; see solver_settings.cu. - semi_continuous_big_m: field_num: 34 + optional: true # MIP heuristic hyper-parameters (nested: settings.heuristic_params.) # Tuning knobs for MIP GPU heuristics; see mip_heuristics_hyper_params_t # in cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp. - # All fields carry non-zero defaults on the C++ side, so an old client - # predating any of these fields will silently revert the corresponding - # value to the proto3 wire default (0) on the server. This matches the - # pre-existing behavior of mip_settings.probing / barrier settings, and - # is the price of using bare scalars in proto3 (no field-presence). + # All fields carry non-zero defaults on the C++ side, so they are declared + # `optional` in the proto: a client that omits any of them gets the C++ + # default rather than the proto3 wire zero (0). - heuristic_params: - population_size: field_num: 35 type: int32 + optional: true - num_cpufj_threads: field_num: 36 type: int32 + optional: true - presolve_time_ratio: field_num: 37 + optional: true - presolve_max_time: field_num: 38 + optional: true - root_lp_time_ratio: field_num: 39 + optional: true - root_lp_max_time: field_num: 40 + optional: true - rins_time_limit: field_num: 41 + optional: true - rins_max_time_limit: field_num: 42 + optional: true - rins_fix_rate: field_num: 43 + optional: true - stagnation_trigger: field_num: 44 type: int32 + optional: true - max_iterations_without_improvement: field_num: 45 type: int32 + optional: true - initial_infeasibility_weight: field_num: 46 + optional: true - n_of_minimums_for_exit: field_num: 47 type: int32 + optional: true - enabled_recombiners: field_num: 48 type: int32 + optional: true - cycle_detection_length: field_num: 49 type: int32 + optional: true - relaxed_lp_time_limit: field_num: 50 + optional: true - related_vars_time_limit: field_num: 51 + optional: true # ───────────────────────────────────────────────────────────────────────────── # Optimization Problem (cpu_optimization_problem_t) diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index 1ec6cd992b..3cf95cd04e 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -636,20 +636,26 @@ def generate_proto_to_settings_body(registry, obj_name, obj, indent=" "): edef = _lookup_enum(registry, ftype) from_fn = _enum_from_proto_fn(ftype, edef) if edef else None + # Two presence mechanisms can each appear independently or together: + # * `optional` -> wrap the body in `if (pb.has_X())` so an omitted + # wire field preserves the C++ struct's in-class default. + # * `sentinel` -> wrap the body (or its remaining inner content) in + # `if (pb.X() )` so a reserved value on the wire is treated + # as "use default" (e.g. -1 for iteration_limit). + # When both are set, the optional guard runs first, then the sentinel + # value-guard runs inside it. + body_lines = [] if sentinel: - # sentinel and optional are mutually exclusive presence mechanisms; - # the validator rejects the combination. Sentinels keep their own - # value-based guard regardless of the optional flag. guard = sentinel["from_proto_guard"] cast = sentinel.get("from_proto_cast", from_proto_cast) - lines.append(f"{ind}if (pb_settings.{pname}() {guard}) {{") expr = ( f"static_cast<{cast}>(pb_settings.{pname}())" if cast else f"pb_settings.{pname}()" ) - lines.append(f"{ind} settings.{cpp_member} = {expr};") - lines.append(f"{ind}}}") + body_lines.append(f"if (pb_settings.{pname}() {guard}) {{") + body_lines.append(f" settings.{cpp_member} = {expr};") + body_lines.append("}") else: if from_fn: rhs = f"{from_fn}(pb_settings.{pname}())" @@ -657,16 +663,16 @@ def generate_proto_to_settings_body(registry, obj_name, obj, indent=" "): rhs = f"static_cast<{from_proto_cast}>(pb_settings.{pname}())" else: rhs = f"pb_settings.{pname}()" + body_lines.append(f"settings.{cpp_member} = {rhs};") - if is_optional: - # `optional` proto3 scalar: only apply the value when the - # client explicitly set it on the wire, so omitted fields - # preserve the C++ struct's in-class default. - lines.append(f"{ind}if (pb_settings.has_{pname}()) {{") - lines.append(f"{ind} settings.{cpp_member} = {rhs};") - lines.append(f"{ind}}}") - else: - lines.append(f"{ind}settings.{cpp_member} = {rhs};") + if is_optional: + lines.append(f"{ind}if (pb_settings.has_{pname}()) {{") + for bl in body_lines: + lines.append(f"{ind} {bl}") + lines.append(f"{ind}}}") + else: + for bl in body_lines: + lines.append(f"{ind}{bl}") return "\n".join(lines) @@ -2156,20 +2162,17 @@ def _validate_registry_uniqueness(registry): ] _check_unique("ArrayFieldId", afid_pairs, errors) - # `optional: true` is only meaningful for scalar proto3 fields that have - # a settable C++ default different from the proto3 zero value. The codegen - # only emits `optional ` + a `has_()` guard for the scalar settings - # path; we reject combinations that would silently do the wrong thing. + # `optional: true` is meaningful for scalar proto3 fields whose C++ + # default differs from the proto3 zero value. The codegen emits + # `optional ` + a `has_()` guard for the scalar settings path. + # It composes with `sentinel`: the optional guard handles "omit ⇒ keep + # default" while the sentinel guard handles "reserved value ⇒ keep + # default" (e.g. -1 for iteration_limit / node_limit). # - # Specifically: - # * Sentinel fields already encode "unset" via a reserved value and the - # mapper uses `from_proto_guard` to recover the C++ default — pairing - # them with `optional` is redundant and the two presence mechanisms - # would fight (`has_X()==true && value==sentinel` is ambiguous). - # * Enum-typed fields are syntactically allowed by proto3 but our enums - # follow the "UNSPECIFIED = 0 means default" convention; flipping them - # to `optional` would change semantics in ways we want to think about - # case-by-case, not implicitly. + # Enum-typed settings fields are intentionally rejected: proto3 allows + # `optional `, but our enums follow the "UNSPECIFIED = 0 means + # default" convention and an implicit flip would change semantics in + # ways that deserve a case-by-case review, not registry-wide policy. for msg, key in [ ("PDLPSolverSettings", "pdlp_settings"), ("MIPSolverSettings", "mip_settings"), @@ -2179,11 +2182,6 @@ def _validate_registry_uniqueness(registry): if not f.get("optional"): continue name = f.get("name") - if f.get("sentinel"): - errors.append( - f"{msg}.{name}: `optional` and `sentinel` are mutually " - "exclusive presence mechanisms" - ) ftype = f.get("type", "double") if _lookup_enum(registry, ftype): errors.append( diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 9933210df0..4d1469311c 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -126,93 +126,93 @@ message OptimizationProblem { } message PDLPSolverSettings { - double absolute_gap_tolerance = 1; - double relative_gap_tolerance = 2; - double primal_infeasible_tolerance = 3; - double dual_infeasible_tolerance = 4; - double absolute_dual_tolerance = 5; - double relative_dual_tolerance = 6; - double absolute_primal_tolerance = 7; - double relative_primal_tolerance = 8; - double time_limit = 9; - int64 iteration_limit = 10; - bool log_to_console = 11; + optional double absolute_gap_tolerance = 1; + optional double relative_gap_tolerance = 2; + optional double primal_infeasible_tolerance = 3; + optional double dual_infeasible_tolerance = 4; + optional double absolute_dual_tolerance = 5; + optional double relative_dual_tolerance = 6; + optional double absolute_primal_tolerance = 7; + optional double relative_primal_tolerance = 8; + optional double time_limit = 9; + optional int64 iteration_limit = 10; + optional bool log_to_console = 11; bool detect_infeasibility = 12; bool strict_infeasibility = 13; PDLPSolverMode pdlp_solver_mode = 14; LPMethod method = 15; - int32 presolver = 16; + optional int32 presolver = 16; optional bool dual_postsolve = 17; bool crossover = 18; - int32 num_gpus = 19; + optional int32 num_gpus = 19; bool per_constraint_residual = 20; bool cudss_deterministic = 21; - int32 folding = 22; - int32 augmented = 23; - int32 dualize = 24; - int32 ordering = 25; - int32 barrier_dual_initial_point = 26; - bool eliminate_dense_columns = 27; + optional int32 folding = 22; + optional int32 augmented = 23; + optional int32 dualize = 24; + optional int32 ordering = 25; + optional int32 barrier_dual_initial_point = 26; + optional bool eliminate_dense_columns = 27; bool save_best_primal_so_far = 28; bool first_primal_feasible = 29; - int32 pdlp_precision = 30; + optional int32 pdlp_precision = 30; optional bool barrier_iterative_refinement = 31; - double barrier_step_scale = 32; + optional double barrier_step_scale = 32; PDLPWarmStartData warm_start_data = 50; } message MIPSolverSettings { - double time_limit = 1; - double relative_mip_gap = 2; - double absolute_mip_gap = 3; - double integrality_tolerance = 4; - double absolute_tolerance = 5; - double relative_tolerance = 6; - double presolve_absolute_tolerance = 7; - bool log_to_console = 8; + optional double time_limit = 1; + optional double relative_mip_gap = 2; + optional double absolute_mip_gap = 3; + optional double integrality_tolerance = 4; + optional double absolute_tolerance = 5; + optional double relative_tolerance = 6; + optional double presolve_absolute_tolerance = 7; + optional bool log_to_console = 8; bool heuristics_only = 9; - int32 num_cpu_threads = 10; - int32 num_gpus = 11; - int32 presolver = 12; - int32 mip_scaling = 13; - double work_limit = 14; - int32 node_limit = 15; - int32 reliability_branching = 16; + optional int32 num_cpu_threads = 10; + optional int32 num_gpus = 11; + optional int32 presolver = 12; + optional int32 mip_scaling = 13; + optional double work_limit = 14; + optional int32 node_limit = 15; + optional int32 reliability_branching = 16; int32 mip_batch_pdlp_strong_branching = 17; - int32 max_cut_passes = 18; - int32 mir_cuts = 19; - int32 mixed_integer_gomory_cuts = 20; - int32 knapsack_cuts = 21; - int32 clique_cuts = 22; - int32 strong_chvatal_gomory_cuts = 23; - int32 reduced_cost_strengthening = 24; - double cut_change_threshold = 25; - double cut_min_orthogonality = 26; + optional int32 max_cut_passes = 18; + optional int32 mir_cuts = 19; + optional int32 mixed_integer_gomory_cuts = 20; + optional int32 knapsack_cuts = 21; + optional int32 clique_cuts = 22; + optional int32 strong_chvatal_gomory_cuts = 23; + optional int32 reduced_cost_strengthening = 24; + optional double cut_change_threshold = 25; + optional double cut_min_orthogonality = 26; int32 determinism_mode = 27; - int32 seed = 28; + optional int32 seed = 28; optional bool probing = 29; - int32 strong_branching_simplex_iteration_limit = 30; - int32 implied_bound_cuts = 31; + optional int32 strong_branching_simplex_iteration_limit = 30; + optional int32 implied_bound_cuts = 31; int32 mip_batch_pdlp_reliability_branching = 32; - int32 symmetry = 33; - double semi_continuous_big_m = 34; - int32 population_size = 35; - int32 num_cpufj_threads = 36; - double presolve_time_ratio = 37; - double presolve_max_time = 38; - double root_lp_time_ratio = 39; - double root_lp_max_time = 40; - double rins_time_limit = 41; - double rins_max_time_limit = 42; - double rins_fix_rate = 43; - int32 stagnation_trigger = 44; - int32 max_iterations_without_improvement = 45; - double initial_infeasibility_weight = 46; - int32 n_of_minimums_for_exit = 47; - int32 enabled_recombiners = 48; - int32 cycle_detection_length = 49; - double relaxed_lp_time_limit = 50; - double related_vars_time_limit = 51; + optional int32 symmetry = 33; + optional double semi_continuous_big_m = 34; + optional int32 population_size = 35; + optional int32 num_cpufj_threads = 36; + optional double presolve_time_ratio = 37; + optional double presolve_max_time = 38; + optional double root_lp_time_ratio = 39; + optional double root_lp_max_time = 40; + optional double rins_time_limit = 41; + optional double rins_max_time_limit = 42; + optional double rins_fix_rate = 43; + optional int32 stagnation_trigger = 44; + optional int32 max_iterations_without_improvement = 45; + optional double initial_infeasibility_weight = 46; + optional int32 n_of_minimums_for_exit = 47; + optional int32 enabled_recombiners = 48; + optional int32 cycle_detection_length = 49; + optional double relaxed_lp_time_limit = 50; + optional double related_vars_time_limit = 51; } message PDLPWarmStartData { diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc index bcc48d2660..f1d4f40cae 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_mip_settings.inc @@ -2,58 +2,150 @@ // AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml // DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py // ============================================================================ - settings.time_limit = pb_settings.time_limit(); - settings.tolerances.relative_mip_gap = pb_settings.relative_mip_gap(); - settings.tolerances.absolute_mip_gap = pb_settings.absolute_mip_gap(); - settings.tolerances.integrality_tolerance = pb_settings.integrality_tolerance(); - settings.tolerances.absolute_tolerance = pb_settings.absolute_tolerance(); - settings.tolerances.relative_tolerance = pb_settings.relative_tolerance(); - settings.tolerances.presolve_absolute_tolerance = pb_settings.presolve_absolute_tolerance(); - settings.log_to_console = pb_settings.log_to_console(); + if (pb_settings.has_time_limit()) { + settings.time_limit = pb_settings.time_limit(); + } + if (pb_settings.has_relative_mip_gap()) { + settings.tolerances.relative_mip_gap = pb_settings.relative_mip_gap(); + } + if (pb_settings.has_absolute_mip_gap()) { + settings.tolerances.absolute_mip_gap = pb_settings.absolute_mip_gap(); + } + if (pb_settings.has_integrality_tolerance()) { + settings.tolerances.integrality_tolerance = pb_settings.integrality_tolerance(); + } + if (pb_settings.has_absolute_tolerance()) { + settings.tolerances.absolute_tolerance = pb_settings.absolute_tolerance(); + } + if (pb_settings.has_relative_tolerance()) { + settings.tolerances.relative_tolerance = pb_settings.relative_tolerance(); + } + if (pb_settings.has_presolve_absolute_tolerance()) { + settings.tolerances.presolve_absolute_tolerance = pb_settings.presolve_absolute_tolerance(); + } + if (pb_settings.has_log_to_console()) { + settings.log_to_console = pb_settings.log_to_console(); + } settings.heuristics_only = pb_settings.heuristics_only(); - settings.num_cpu_threads = pb_settings.num_cpu_threads(); - settings.num_gpus = pb_settings.num_gpus(); - settings.presolver = static_cast(pb_settings.presolver()); - settings.mip_scaling = pb_settings.mip_scaling(); - settings.symmetry = pb_settings.symmetry(); - settings.work_limit = pb_settings.work_limit(); - if (pb_settings.node_limit() >= 0) { - settings.node_limit = static_cast(pb_settings.node_limit()); - } - settings.reliability_branching = pb_settings.reliability_branching(); + if (pb_settings.has_num_cpu_threads()) { + settings.num_cpu_threads = pb_settings.num_cpu_threads(); + } + if (pb_settings.has_num_gpus()) { + settings.num_gpus = pb_settings.num_gpus(); + } + if (pb_settings.has_presolver()) { + settings.presolver = static_cast(pb_settings.presolver()); + } + if (pb_settings.has_mip_scaling()) { + settings.mip_scaling = pb_settings.mip_scaling(); + } + if (pb_settings.has_symmetry()) { + settings.symmetry = pb_settings.symmetry(); + } + if (pb_settings.has_work_limit()) { + settings.work_limit = pb_settings.work_limit(); + } + if (pb_settings.has_node_limit()) { + if (pb_settings.node_limit() >= 0) { + settings.node_limit = static_cast(pb_settings.node_limit()); + } + } + if (pb_settings.has_reliability_branching()) { + settings.reliability_branching = pb_settings.reliability_branching(); + } settings.mip_batch_pdlp_strong_branching = pb_settings.mip_batch_pdlp_strong_branching(); settings.mip_batch_pdlp_reliability_branching = pb_settings.mip_batch_pdlp_reliability_branching(); - settings.strong_branching_simplex_iteration_limit = pb_settings.strong_branching_simplex_iteration_limit(); - settings.max_cut_passes = pb_settings.max_cut_passes(); - settings.mir_cuts = pb_settings.mir_cuts(); - settings.mixed_integer_gomory_cuts = pb_settings.mixed_integer_gomory_cuts(); - settings.knapsack_cuts = pb_settings.knapsack_cuts(); - settings.clique_cuts = pb_settings.clique_cuts(); - settings.implied_bound_cuts = pb_settings.implied_bound_cuts(); - settings.strong_chvatal_gomory_cuts = pb_settings.strong_chvatal_gomory_cuts(); - settings.reduced_cost_strengthening = pb_settings.reduced_cost_strengthening(); - settings.cut_change_threshold = pb_settings.cut_change_threshold(); - settings.cut_min_orthogonality = pb_settings.cut_min_orthogonality(); + if (pb_settings.has_strong_branching_simplex_iteration_limit()) { + settings.strong_branching_simplex_iteration_limit = pb_settings.strong_branching_simplex_iteration_limit(); + } + if (pb_settings.has_max_cut_passes()) { + settings.max_cut_passes = pb_settings.max_cut_passes(); + } + if (pb_settings.has_mir_cuts()) { + settings.mir_cuts = pb_settings.mir_cuts(); + } + if (pb_settings.has_mixed_integer_gomory_cuts()) { + settings.mixed_integer_gomory_cuts = pb_settings.mixed_integer_gomory_cuts(); + } + if (pb_settings.has_knapsack_cuts()) { + settings.knapsack_cuts = pb_settings.knapsack_cuts(); + } + if (pb_settings.has_clique_cuts()) { + settings.clique_cuts = pb_settings.clique_cuts(); + } + if (pb_settings.has_implied_bound_cuts()) { + settings.implied_bound_cuts = pb_settings.implied_bound_cuts(); + } + if (pb_settings.has_strong_chvatal_gomory_cuts()) { + settings.strong_chvatal_gomory_cuts = pb_settings.strong_chvatal_gomory_cuts(); + } + if (pb_settings.has_reduced_cost_strengthening()) { + settings.reduced_cost_strengthening = pb_settings.reduced_cost_strengthening(); + } + if (pb_settings.has_cut_change_threshold()) { + settings.cut_change_threshold = pb_settings.cut_change_threshold(); + } + if (pb_settings.has_cut_min_orthogonality()) { + settings.cut_min_orthogonality = pb_settings.cut_min_orthogonality(); + } settings.determinism_mode = pb_settings.determinism_mode(); - settings.seed = pb_settings.seed(); + if (pb_settings.has_seed()) { + settings.seed = pb_settings.seed(); + } if (pb_settings.has_probing()) { settings.probing = pb_settings.probing(); } - settings.semi_continuous_big_m = pb_settings.semi_continuous_big_m(); - settings.heuristic_params.population_size = pb_settings.population_size(); - settings.heuristic_params.num_cpufj_threads = pb_settings.num_cpufj_threads(); - settings.heuristic_params.presolve_time_ratio = pb_settings.presolve_time_ratio(); - settings.heuristic_params.presolve_max_time = pb_settings.presolve_max_time(); - settings.heuristic_params.root_lp_time_ratio = pb_settings.root_lp_time_ratio(); - settings.heuristic_params.root_lp_max_time = pb_settings.root_lp_max_time(); - settings.heuristic_params.rins_time_limit = pb_settings.rins_time_limit(); - settings.heuristic_params.rins_max_time_limit = pb_settings.rins_max_time_limit(); - settings.heuristic_params.rins_fix_rate = pb_settings.rins_fix_rate(); - settings.heuristic_params.stagnation_trigger = pb_settings.stagnation_trigger(); - settings.heuristic_params.max_iterations_without_improvement = pb_settings.max_iterations_without_improvement(); - settings.heuristic_params.initial_infeasibility_weight = pb_settings.initial_infeasibility_weight(); - settings.heuristic_params.n_of_minimums_for_exit = pb_settings.n_of_minimums_for_exit(); - settings.heuristic_params.enabled_recombiners = pb_settings.enabled_recombiners(); - settings.heuristic_params.cycle_detection_length = pb_settings.cycle_detection_length(); - settings.heuristic_params.relaxed_lp_time_limit = pb_settings.relaxed_lp_time_limit(); - settings.heuristic_params.related_vars_time_limit = pb_settings.related_vars_time_limit(); + if (pb_settings.has_semi_continuous_big_m()) { + settings.semi_continuous_big_m = pb_settings.semi_continuous_big_m(); + } + if (pb_settings.has_population_size()) { + settings.heuristic_params.population_size = pb_settings.population_size(); + } + if (pb_settings.has_num_cpufj_threads()) { + settings.heuristic_params.num_cpufj_threads = pb_settings.num_cpufj_threads(); + } + if (pb_settings.has_presolve_time_ratio()) { + settings.heuristic_params.presolve_time_ratio = pb_settings.presolve_time_ratio(); + } + if (pb_settings.has_presolve_max_time()) { + settings.heuristic_params.presolve_max_time = pb_settings.presolve_max_time(); + } + if (pb_settings.has_root_lp_time_ratio()) { + settings.heuristic_params.root_lp_time_ratio = pb_settings.root_lp_time_ratio(); + } + if (pb_settings.has_root_lp_max_time()) { + settings.heuristic_params.root_lp_max_time = pb_settings.root_lp_max_time(); + } + if (pb_settings.has_rins_time_limit()) { + settings.heuristic_params.rins_time_limit = pb_settings.rins_time_limit(); + } + if (pb_settings.has_rins_max_time_limit()) { + settings.heuristic_params.rins_max_time_limit = pb_settings.rins_max_time_limit(); + } + if (pb_settings.has_rins_fix_rate()) { + settings.heuristic_params.rins_fix_rate = pb_settings.rins_fix_rate(); + } + if (pb_settings.has_stagnation_trigger()) { + settings.heuristic_params.stagnation_trigger = pb_settings.stagnation_trigger(); + } + if (pb_settings.has_max_iterations_without_improvement()) { + settings.heuristic_params.max_iterations_without_improvement = pb_settings.max_iterations_without_improvement(); + } + if (pb_settings.has_initial_infeasibility_weight()) { + settings.heuristic_params.initial_infeasibility_weight = pb_settings.initial_infeasibility_weight(); + } + if (pb_settings.has_n_of_minimums_for_exit()) { + settings.heuristic_params.n_of_minimums_for_exit = pb_settings.n_of_minimums_for_exit(); + } + if (pb_settings.has_enabled_recombiners()) { + settings.heuristic_params.enabled_recombiners = pb_settings.enabled_recombiners(); + } + if (pb_settings.has_cycle_detection_length()) { + settings.heuristic_params.cycle_detection_length = pb_settings.cycle_detection_length(); + } + if (pb_settings.has_relaxed_lp_time_limit()) { + settings.heuristic_params.relaxed_lp_time_limit = pb_settings.relaxed_lp_time_limit(); + } + if (pb_settings.has_related_vars_time_limit()) { + settings.heuristic_params.related_vars_time_limit = pb_settings.related_vars_time_limit(); + } diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc index 446b58ab94..f80a74c65a 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc @@ -2,41 +2,83 @@ // AUTO-GENERATED by src/grpc/codegen/generate_conversions.py from field_registry.yaml // DO NOT EDIT MANUALLY — regenerate with: python generate_conversions.py // ============================================================================ - settings.tolerances.absolute_gap_tolerance = pb_settings.absolute_gap_tolerance(); - settings.tolerances.relative_gap_tolerance = pb_settings.relative_gap_tolerance(); - settings.tolerances.primal_infeasible_tolerance = pb_settings.primal_infeasible_tolerance(); - settings.tolerances.dual_infeasible_tolerance = pb_settings.dual_infeasible_tolerance(); - settings.tolerances.absolute_dual_tolerance = pb_settings.absolute_dual_tolerance(); - settings.tolerances.relative_dual_tolerance = pb_settings.relative_dual_tolerance(); - settings.tolerances.absolute_primal_tolerance = pb_settings.absolute_primal_tolerance(); - settings.tolerances.relative_primal_tolerance = pb_settings.relative_primal_tolerance(); - settings.time_limit = pb_settings.time_limit(); - if (pb_settings.iteration_limit() >= 0) { - settings.iteration_limit = static_cast(pb_settings.iteration_limit()); - } - settings.log_to_console = pb_settings.log_to_console(); + if (pb_settings.has_absolute_gap_tolerance()) { + settings.tolerances.absolute_gap_tolerance = pb_settings.absolute_gap_tolerance(); + } + if (pb_settings.has_relative_gap_tolerance()) { + settings.tolerances.relative_gap_tolerance = pb_settings.relative_gap_tolerance(); + } + if (pb_settings.has_primal_infeasible_tolerance()) { + settings.tolerances.primal_infeasible_tolerance = pb_settings.primal_infeasible_tolerance(); + } + if (pb_settings.has_dual_infeasible_tolerance()) { + settings.tolerances.dual_infeasible_tolerance = pb_settings.dual_infeasible_tolerance(); + } + if (pb_settings.has_absolute_dual_tolerance()) { + settings.tolerances.absolute_dual_tolerance = pb_settings.absolute_dual_tolerance(); + } + if (pb_settings.has_relative_dual_tolerance()) { + settings.tolerances.relative_dual_tolerance = pb_settings.relative_dual_tolerance(); + } + if (pb_settings.has_absolute_primal_tolerance()) { + settings.tolerances.absolute_primal_tolerance = pb_settings.absolute_primal_tolerance(); + } + if (pb_settings.has_relative_primal_tolerance()) { + settings.tolerances.relative_primal_tolerance = pb_settings.relative_primal_tolerance(); + } + if (pb_settings.has_time_limit()) { + settings.time_limit = pb_settings.time_limit(); + } + if (pb_settings.has_iteration_limit()) { + if (pb_settings.iteration_limit() >= 0) { + settings.iteration_limit = static_cast(pb_settings.iteration_limit()); + } + } + if (pb_settings.has_log_to_console()) { + settings.log_to_console = pb_settings.log_to_console(); + } settings.detect_infeasibility = pb_settings.detect_infeasibility(); settings.strict_infeasibility = pb_settings.strict_infeasibility(); settings.pdlp_solver_mode = from_proto_pdlp_solver_mode(pb_settings.pdlp_solver_mode()); settings.method = from_proto_lp_method(pb_settings.method()); - settings.presolver = static_cast(pb_settings.presolver()); + if (pb_settings.has_presolver()) { + settings.presolver = static_cast(pb_settings.presolver()); + } if (pb_settings.has_dual_postsolve()) { settings.dual_postsolve = pb_settings.dual_postsolve(); } settings.crossover = pb_settings.crossover(); - settings.num_gpus = pb_settings.num_gpus(); + if (pb_settings.has_num_gpus()) { + settings.num_gpus = pb_settings.num_gpus(); + } settings.per_constraint_residual = pb_settings.per_constraint_residual(); settings.cudss_deterministic = pb_settings.cudss_deterministic(); - settings.folding = pb_settings.folding(); - settings.augmented = pb_settings.augmented(); - settings.dualize = pb_settings.dualize(); - settings.ordering = pb_settings.ordering(); - settings.barrier_dual_initial_point = pb_settings.barrier_dual_initial_point(); - settings.eliminate_dense_columns = pb_settings.eliminate_dense_columns(); + if (pb_settings.has_folding()) { + settings.folding = pb_settings.folding(); + } + if (pb_settings.has_augmented()) { + settings.augmented = pb_settings.augmented(); + } + if (pb_settings.has_dualize()) { + settings.dualize = pb_settings.dualize(); + } + if (pb_settings.has_ordering()) { + settings.ordering = pb_settings.ordering(); + } + if (pb_settings.has_barrier_dual_initial_point()) { + settings.barrier_dual_initial_point = pb_settings.barrier_dual_initial_point(); + } + if (pb_settings.has_eliminate_dense_columns()) { + settings.eliminate_dense_columns = pb_settings.eliminate_dense_columns(); + } if (pb_settings.has_barrier_iterative_refinement()) { settings.barrier_iterative_refinement = pb_settings.barrier_iterative_refinement(); } - settings.barrier_step_scale = pb_settings.barrier_step_scale(); + if (pb_settings.has_barrier_step_scale()) { + settings.barrier_step_scale = pb_settings.barrier_step_scale(); + } settings.save_best_primal_so_far = pb_settings.save_best_primal_so_far(); settings.first_primal_feasible = pb_settings.first_primal_feasible(); - settings.pdlp_precision = static_cast(pb_settings.pdlp_precision()); + if (pb_settings.has_pdlp_precision()) { + settings.pdlp_precision = static_cast(pb_settings.pdlp_precision()); + } diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index dedd055430..d69361ce8c 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -2142,3 +2142,113 @@ TEST(MapperRoundtrip, PDLPSettingsBarrierIterativeRefinementExplicitFalseRoundtr map_proto_to_pdlp_settings(pb, restored); EXPECT_FALSE(restored.barrier_iterative_refinement); } + +// Wide-coverage sanity: a default-constructed proto (no fields touched on the +// wire) must, after the mapper, leave every C++ scalar settings field at its +// in-class default. Spot-checks a representative cross-section of the fields +// converted to `optional` in field_registry.yaml — booleans with default true, +// numeric tolerances with non-zero defaults, time/work limits at infinity, +// int32 knobs whose default is -1 or +N, and a handful of heuristic_params +// values. If a field is ever added with a non-zero C++ default but without +// `optional: true` in the registry, this test will fail and point at the gap. +TEST(MapperRoundtrip, PDLPSettingsDefaultProtoPreservesAllCppDefaults) +{ + cuopt::remote::PDLPSolverSettings pb; + pdlp_solver_settings_t fresh; + pdlp_solver_settings_t after = fresh; + map_proto_to_pdlp_settings(pb, after); + + // Tolerances (all 1e-4 / 1e-10 by C++ default). + EXPECT_DOUBLE_EQ(after.tolerances.absolute_gap_tolerance, + fresh.tolerances.absolute_gap_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.relative_gap_tolerance, + fresh.tolerances.relative_gap_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.absolute_primal_tolerance, + fresh.tolerances.absolute_primal_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.relative_primal_tolerance, + fresh.tolerances.relative_primal_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.absolute_dual_tolerance, + fresh.tolerances.absolute_dual_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.relative_dual_tolerance, + fresh.tolerances.relative_dual_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.primal_infeasible_tolerance, + fresh.tolerances.primal_infeasible_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.dual_infeasible_tolerance, + fresh.tolerances.dual_infeasible_tolerance); + // Limits. + EXPECT_EQ(after.time_limit, fresh.time_limit) << "time_limit default (infinity) preserved"; + // Bools with non-zero defaults. + EXPECT_EQ(after.log_to_console, fresh.log_to_console); + EXPECT_EQ(after.dual_postsolve, fresh.dual_postsolve); + EXPECT_EQ(after.eliminate_dense_columns, fresh.eliminate_dense_columns); + EXPECT_EQ(after.barrier_iterative_refinement, fresh.barrier_iterative_refinement); + // Numeric defaults != 0. + EXPECT_EQ(after.num_gpus, fresh.num_gpus); + EXPECT_EQ(after.folding, fresh.folding); + EXPECT_EQ(after.augmented, fresh.augmented); + EXPECT_EQ(after.dualize, fresh.dualize); + EXPECT_EQ(after.ordering, fresh.ordering); + EXPECT_EQ(after.barrier_dual_initial_point, fresh.barrier_dual_initial_point); + EXPECT_DOUBLE_EQ(after.barrier_step_scale, fresh.barrier_step_scale); + // Enum-int32 fields (post-decode clamping defends out-of-range; default `0` + // on the wire is in-range so the clamp does not fire, but the `optional` + // guard prevents the assignment entirely and the C++ default survives). + EXPECT_EQ(static_cast(after.presolver), static_cast(fresh.presolver)); + EXPECT_EQ(static_cast(after.pdlp_precision), static_cast(fresh.pdlp_precision)); +} + +TEST(MapperRoundtrip, MIPSettingsDefaultProtoPreservesAllCppDefaults) +{ + cuopt::remote::MIPSolverSettings pb; + mip_solver_settings_t fresh; + mip_solver_settings_t after = fresh; + map_proto_to_mip_settings(pb, after); + + // Tolerances. + EXPECT_DOUBLE_EQ(after.tolerances.absolute_mip_gap, fresh.tolerances.absolute_mip_gap); + EXPECT_DOUBLE_EQ(after.tolerances.relative_mip_gap, fresh.tolerances.relative_mip_gap); + EXPECT_DOUBLE_EQ(after.tolerances.integrality_tolerance, fresh.tolerances.integrality_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.absolute_tolerance, fresh.tolerances.absolute_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.relative_tolerance, fresh.tolerances.relative_tolerance); + EXPECT_DOUBLE_EQ(after.tolerances.presolve_absolute_tolerance, + fresh.tolerances.presolve_absolute_tolerance); + // Limits. + EXPECT_EQ(after.time_limit, fresh.time_limit); + EXPECT_EQ(after.work_limit, fresh.work_limit); + EXPECT_EQ(after.node_limit, fresh.node_limit); // sentinel path + // Bools with non-zero defaults. + EXPECT_EQ(after.log_to_console, fresh.log_to_console); + EXPECT_EQ(after.probing, fresh.probing); + // Numeric knobs. + EXPECT_EQ(after.num_cpu_threads, fresh.num_cpu_threads); + EXPECT_EQ(after.num_gpus, fresh.num_gpus); + EXPECT_EQ(after.reliability_branching, fresh.reliability_branching); + EXPECT_EQ(after.symmetry, fresh.symmetry); // clamp-defended; default -1 + EXPECT_EQ(after.max_cut_passes, fresh.max_cut_passes); + EXPECT_EQ(after.mir_cuts, fresh.mir_cuts); + EXPECT_EQ(after.mixed_integer_gomory_cuts, fresh.mixed_integer_gomory_cuts); + EXPECT_EQ(after.knapsack_cuts, fresh.knapsack_cuts); + EXPECT_EQ(after.clique_cuts, fresh.clique_cuts); + EXPECT_EQ(after.implied_bound_cuts, fresh.implied_bound_cuts); + EXPECT_EQ(after.strong_chvatal_gomory_cuts, fresh.strong_chvatal_gomory_cuts); + EXPECT_EQ(after.reduced_cost_strengthening, fresh.reduced_cost_strengthening); + EXPECT_DOUBLE_EQ(after.cut_change_threshold, fresh.cut_change_threshold); + EXPECT_DOUBLE_EQ(after.cut_min_orthogonality, fresh.cut_min_orthogonality); + EXPECT_EQ(after.strong_branching_simplex_iteration_limit, + fresh.strong_branching_simplex_iteration_limit); + EXPECT_EQ(after.seed, fresh.seed); + EXPECT_DOUBLE_EQ(after.semi_continuous_big_m, fresh.semi_continuous_big_m); + EXPECT_EQ(static_cast(after.presolver), static_cast(fresh.presolver)); + EXPECT_EQ(after.mip_scaling, fresh.mip_scaling); + // heuristic_params: spot-check one of each kind (int, double). + EXPECT_EQ(after.heuristic_params.population_size, fresh.heuristic_params.population_size); + EXPECT_EQ(after.heuristic_params.num_cpufj_threads, fresh.heuristic_params.num_cpufj_threads); + EXPECT_DOUBLE_EQ(after.heuristic_params.presolve_time_ratio, + fresh.heuristic_params.presolve_time_ratio); + EXPECT_DOUBLE_EQ(after.heuristic_params.presolve_max_time, + fresh.heuristic_params.presolve_max_time); + EXPECT_DOUBLE_EQ(after.heuristic_params.rins_fix_rate, fresh.heuristic_params.rins_fix_rate); + EXPECT_EQ(after.heuristic_params.enabled_recombiners, fresh.heuristic_params.enabled_recombiners); + EXPECT_DOUBLE_EQ(after.heuristic_params.initial_infeasibility_weight, + fresh.heuristic_params.initial_infeasibility_weight); +} From 74bc17e3712030738f777806071ab4c0b89d116e Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Wed, 27 May 2026 19:05:08 -0400 Subject: [PATCH 14/15] grpc/codegen: extend `optional` to enum settings with default-mismatch currently this only applies to pdlp_solver_mode where the C++ default does not match the proto3 zero value. The other enums already align. --- cpp/src/grpc/codegen/field_registry.yaml | 10 ++++++- cpp/src/grpc/codegen/generate_conversions.py | 30 +++++-------------- .../codegen/generated/cuopt_remote_data.proto | 2 +- .../generated_proto_to_pdlp_settings.inc | 4 ++- .../grpc/grpc_client_test.cpp | 7 +++++ 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/cpp/src/grpc/codegen/field_registry.yaml b/cpp/src/grpc/codegen/field_registry.yaml index ceaf4b8d70..2ec60fc3bc 100644 --- a/cpp/src/grpc/codegen/field_registry.yaml +++ b/cpp/src/grpc/codegen/field_registry.yaml @@ -58,7 +58,9 @@ # silently overwriting it with the proto3 zero. Composes # with `sentinel` (optional guards the omitted case; # sentinel keeps the existing wire encoding for "explicit -# default" values). Not supported on enum types. +# default" values). Also valid on enum-typed fields whose +# proto3 zero (the first listed value) does not match the +# documented C++ default — e.g. pdlp_solver_mode. # to_proto_cast – explicit cast type for C++ -> proto direction # from_proto_cast – explicit cast type for proto -> C++ direction # @@ -471,8 +473,14 @@ pdlp_settings: field_num: 13 type: bool - pdlp_solver_mode: + # C++ default is `Stable3` (enum value 4). The proto3 enum zero is + # `Stable1` (the first value declared), so without `optional` an + # omitted field would silently apply `Stable1` instead of the cuOpt + # default. Declared `optional` so the mapper preserves the C++ + # default via `has_pdlp_solver_mode()`. field_num: 14 type: pdlp_solver_mode + optional: true - method: field_num: 15 type: lp_method diff --git a/cpp/src/grpc/codegen/generate_conversions.py b/cpp/src/grpc/codegen/generate_conversions.py index 3cf95cd04e..e5e35f0f32 100644 --- a/cpp/src/grpc/codegen/generate_conversions.py +++ b/cpp/src/grpc/codegen/generate_conversions.py @@ -2162,33 +2162,17 @@ def _validate_registry_uniqueness(registry): ] _check_unique("ArrayFieldId", afid_pairs, errors) - # `optional: true` is meaningful for scalar proto3 fields whose C++ + # `optional: true` is meaningful for any proto3 settings field whose C++ # default differs from the proto3 zero value. The codegen emits # `optional ` + a `has_()` guard for the scalar settings path. # It composes with `sentinel`: the optional guard handles "omit ⇒ keep # default" while the sentinel guard handles "reserved value ⇒ keep - # default" (e.g. -1 for iteration_limit / node_limit). - # - # Enum-typed settings fields are intentionally rejected: proto3 allows - # `optional `, but our enums follow the "UNSPECIFIED = 0 means - # default" convention and an implicit flip would change semantics in - # ways that deserve a case-by-case review, not registry-wide policy. - for msg, key in [ - ("PDLPSolverSettings", "pdlp_settings"), - ("MIPSolverSettings", "mip_settings"), - ]: - section = registry.get(key) or {} - for f in parse_settings_fields(section.get("fields", [])): - if not f.get("optional"): - continue - name = f.get("name") - ftype = f.get("type", "double") - if _lookup_enum(registry, ftype): - errors.append( - f"{msg}.{name}: `optional` is not supported on enum-typed " - f"fields (type {ftype!r}); enums use the UNSPECIFIED=0 " - "convention for defaults" - ) + # default" (e.g. -1 for iteration_limit / node_limit). It also composes + # with enum-typed fields: the `has_()` guard wraps the enum + # from_proto conversion so an omitted field preserves the C++ default + # for enums whose proto3 zero (the first value listed in the registry) + # does not coincide with the documented C++ default — e.g. + # pdlp_solver_mode (proto-zero=Stable1, C++ default=Stable3). if errors: raise ValueError( diff --git a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto index 4d1469311c..626f91bf15 100644 --- a/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto +++ b/cpp/src/grpc/codegen/generated/cuopt_remote_data.proto @@ -139,7 +139,7 @@ message PDLPSolverSettings { optional bool log_to_console = 11; bool detect_infeasibility = 12; bool strict_infeasibility = 13; - PDLPSolverMode pdlp_solver_mode = 14; + optional PDLPSolverMode pdlp_solver_mode = 14; LPMethod method = 15; optional int32 presolver = 16; optional bool dual_postsolve = 17; diff --git a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc index f80a74c65a..9c5343aa7c 100644 --- a/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc +++ b/cpp/src/grpc/codegen/generated/generated_proto_to_pdlp_settings.inc @@ -39,7 +39,9 @@ } settings.detect_infeasibility = pb_settings.detect_infeasibility(); settings.strict_infeasibility = pb_settings.strict_infeasibility(); - settings.pdlp_solver_mode = from_proto_pdlp_solver_mode(pb_settings.pdlp_solver_mode()); + if (pb_settings.has_pdlp_solver_mode()) { + settings.pdlp_solver_mode = from_proto_pdlp_solver_mode(pb_settings.pdlp_solver_mode()); + } settings.method = from_proto_lp_method(pb_settings.method()); if (pb_settings.has_presolver()) { settings.presolver = static_cast(pb_settings.presolver()); diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index d69361ce8c..69302f1740 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -2195,6 +2195,13 @@ TEST(MapperRoundtrip, PDLPSettingsDefaultProtoPreservesAllCppDefaults) // guard prevents the assignment entirely and the C++ default survives). EXPECT_EQ(static_cast(after.presolver), static_cast(fresh.presolver)); EXPECT_EQ(static_cast(after.pdlp_precision), static_cast(fresh.pdlp_precision)); + // True-enum field: the proto3 enum zero is `Stable1` (first listed value) + // and the C++ default is `Stable3`. Without `optional` on this field the + // mapper would silently apply `Stable1`; the `has_pdlp_solver_mode()` + // guard preserves the C++ default. + EXPECT_EQ(static_cast(after.pdlp_solver_mode), static_cast(fresh.pdlp_solver_mode)); + EXPECT_EQ(static_cast(fresh.pdlp_solver_mode), static_cast(pdlp_solver_mode_t::Stable3)) + << "pre-condition: C++ default is expected to be Stable3"; } TEST(MapperRoundtrip, MIPSettingsDefaultProtoPreservesAllCppDefaults) From 776ebc0ce1c9af2775e568d2fefd13b1a2ee4d27 Mon Sep 17 00:00:00 2001 From: Trevor McKay Date: Thu, 28 May 2026 10:22:49 -0400 Subject: [PATCH 15/15] Repair comment in cpp/tests/linear_programming/grpc/grpc_client_test.cpp Co-authored-by: Miles Lubin --- cpp/tests/linear_programming/grpc/grpc_client_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp index 69302f1740..ed741462fe 100644 --- a/cpp/tests/linear_programming/grpc/grpc_client_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_client_test.cpp @@ -2068,7 +2068,7 @@ TEST(MapperRoundtrip, PDLPSettingsIterationLimitSentinel) // A handful of bool settings have a C++ default of `true` but live on the wire // in a proto3 message. Without `optional`, an omitted field decodes as the // proto3 zero (`false`) and the mapper would silently overwrite the C++ -// default. The codegen now emits `optional ` for these fields and +// default. The codegen emits `optional ` for these fields and // guards the assignment with `has_()`, so an omitted field preserves the // solver default. The tests below pin that behavior for the three currently // converted fields.