Skip to content

Conversation

@akifcorduk
Copy link
Contributor

@akifcorduk akifcorduk commented Jan 15, 2026

This PR adds callbacks to C API. Users can now use C API to implement get_solution and set_solution callbacks.

This PR is a breaking PR that changes callback input/output pointers from device memory to host memory. We are also adding a user_data pointer for user to be more flexible inside the callbacks.

Previously callbacks were not enabled when third party presolved was enabled. Now we are able to handle crushing/uncrushing of solutions when third party presolve is enabled.

The examples and tests are modified to reflect the change.

Summary by CodeRabbit

  • New Features

    • New MIP callback APIs to receive/inject incumbent solutions with optional user_data propagated across C/C++ and Python; solver settings accept user_data when registering callbacks.
    • Presolve integration now exposes reduced↔original variable mappings and a utility to reconstruct full solutions.
  • Tests

    • Added end-to-end MIP callback tests validating invocation, data transfer, and user_data propagation.
  • Documentation

    • Examples updated to demonstrate passing and verifying user_data in callbacks.

✏️ Tip: You can customize this high-level summary in your review settings.

@akifcorduk akifcorduk added this to the 26.02 milestone Jan 15, 2026
@akifcorduk akifcorduk requested a review from a team as a code owner January 15, 2026 14:31
@akifcorduk akifcorduk added the non-breaking Introduces a non-breaking change label Jan 15, 2026
@akifcorduk akifcorduk requested a review from chris-maes January 15, 2026 14:31
@akifcorduk akifcorduk added the improvement Improves an existing functionality label Jan 15, 2026
@akifcorduk akifcorduk requested a review from nguidotti January 15, 2026 14:31
@akifcorduk akifcorduk added the mip label Jan 15, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Adds optional user_data propagation to MIP solution callbacks across C, C++, and Python layers; introduces new C API registration functions for get/set callbacks; extends callback interfaces and invocation sites to accept a user_data pointer; updates bindings, tests, presolve/papilo plumbing, and related I/O conversions.

Changes

Cohort / File(s) Summary
C API - Header & Implementation
cpp/include/cuopt/linear_programming/cuopt_c.h, cpp/src/linear_programming/cuopt_c.cpp
Add cuOptMIPGetSolutionCallback / cuOptMIPSetSolutionCallback typedefs and cuOptSetMIPGetSolutionCallback / cuOptSetMIPSetSolutionCallback functions; add internal solver_settings handle and C->C++ adapter wrappers to store/forward user_data.
C API Tests
cpp/tests/linear_programming/c_api_tests/c_api_test.c, .../c_api_tests.cpp, .../c_api_tests.h
Add test_mip_callbacks() and mip_callback_context_t; register new C API callbacks, validate invocation counts and transferred solution/objective data (duplicate insertion noted in C test file).
Core callback interfaces & implementations (C++)
cpp/include/cuopt/linear_programming/utilities/internals.hpp, .../callbacks_implems.hpp
Add void* user_data member to base_solution_callback_t with set_user_data/get_user_data; change get_solution/set_solution signatures to accept user_data; update default callback implementations to pass user_data into Python bridge and adjust refcounting.
Solver settings API (C++ core)
cpp/include/.../solver_settings.hpp, cpp/include/.../mip/solver_settings.hpp, cpp/src/math_optimization/solver_settings.cu, cpp/src/mip/solver_settings.cu
Extend set_mip_callback to accept optional void* user_data; implementations call callback->set_user_data(user_data) when non-null and preserve existing storage semantics.
Callback invocation sites (MIP internals)
cpp/src/mip/diversity/population.cu, cpp/src/mip/solver_settings.cu
Update calls to get_solution/set_solution to pass callback->get_user_data(); switch some callback I/O to host std::vector with raft::copy and apply papilo crush/uncrush steps around external solution injection.
Presolve / Papilo integration
cpp/src/mip/presolve/third_party_presolve.{hpp,cpp}, cpp/src/mip/problem/presolve_data.{cuh,cu}, cpp/src/mip/problem/{problem.cu,problem.cuh}, cpp/src/mip/CMakeLists.txt, cpp/src/mip/presolve/third_party_presolve.cpp
Add reduced↔original mapping storage and accessors, uncrush_primal_solution, presolve_data_t helpers (pre/post-process assignment/solution), papilo integration setters/getters; wire presolve_result into solve flow; add new source to CMake.
MIP tests & utilities (C++ GPU & host)
cpp/tests/mip/incumbent_callback_test.cu, cpp/tests/linear_programming/utilities/pdlp_test_utilities.cuh, cpp/tests/mip/mip_utils.cuh
Update test callback classes/signatures to accept user_data; convert some rmm::device_uvector usages to host std::vector for callback I/O; add host-based test helpers/overloads and input guards.
Branch & Bound logging
cpp/src/dual_simplex/branch_and_bound.cpp
Add DEBUG log when a proposed solution is not accepted because it does not improve the current upper bound.
Python Cython & bindings
python/cuopt/cuopt/linear_programming/internals/internals.pyx, .../solver/solver.pxd, .../solver/solver_wrapper.pyx
Add _user_data attribute, user_data property, and get_user_data_ptr() to PyCallback; update Cython extern signatures and wrapper to pass callback user_data pointer when registering MIP callbacks; switch NumPy array extraction to ctypes-based approach.
Python high-level API & tests/examples
python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py, python/cuopt/.../tests/*test_incumbent_callbacks.py, python/cuopt/.../tests/*test_python_API.py, python/cuopt_server/.../solver.py, docs/.../incumbent_solutions_example.py, docs/.../incumbent_callback_example.py
Extend set_mip_callback to accept user_data and set callback.user_data; update tests/examples to pass and validate user_data; change solution/cost extraction to use tolist()/float rather than device copy.
C++ public solver settings handle & callback adapters
cpp/src/linear_programming/cuopt_c.cpp
Introduce solver_settings_handle_t and C++ adapter callback classes to manage lifetime and forward C callbacks (with user_data) into internal interfaces.
Miscellaneous
multiple files
SPDX/header year updates and minor doc/comment edits accompanying API changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change in the PR: adding C API callbacks for getting and setting solutions, which is the primary feature introduced across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@akifcorduk akifcorduk requested a review from a team as a code owner January 16, 2026 15:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@cpp/include/cuopt/linear_programming/cuopt_c.h`:
- Around line 706-728: The Doxygen for cuOptSetMipGetSolutionCallback and
cuOptSetMipSetSolutionCallback is missing documentation for the new user_data
parameter; update both comment blocks to add a `@param`[in] user_data description
(e.g., "User-defined pointer passed through to the callback") and mention that
it will be forwarded to the respective
cuOptMipGetSolutionCallback/cuOptMipSetSolutionCallback when invoked so the
public API documents the parameter contract.

In `@cpp/include/cuopt/linear_programming/mip/solver_settings.hpp`:
- Around line 37-41: Update the Doxygen block for set_mip_callback to document
the new user_data parameter: add a brief `@param` description for user_data
explaining it is an opaque pointer passed to the callback (e.g., "pointer to
user-defined data forwarded to the callback"), and ensure the existing `@param`
for callback remains accurate; modify the comment above the function declaration
in solver_settings.hpp (the set_mip_callback declaration) so the public API docs
enumerate both callback and user_data.

In `@cpp/include/cuopt/linear_programming/utilities/internals.hpp`:
- Around line 34-36: Add brief Doxygen comments above the public accessor
methods set_user_data and get_user_data describing their purpose: annotate
set_user_data with "Store user-defined context data for callback invocation."
and get_user_data with "Retrieve user-defined context data passed to callbacks."
Place these comments immediately above the corresponding method declarations in
internals.hpp so the public solver callback interface methods are documented per
the header-file documentation guideline.

In `@cpp/src/mip/diversity/diversity_manager.cu`:
- Around line 446-450: There is an unconditional exit(0) that aborts the solver
flow and prevents LP solution handling; remove the exit(0) invocation so
execution continues into the subsequent LP handling (the if
(ls.lp_optimal_exists) block) after calling clamp_within_var_bounds, ensuring
normal callback and MIP behavior; search for the symbols
clamp_within_var_bounds, exit(0), and ls.lp_optimal_exists to locate and delete
the exit call so control can proceed.

In `@cpp/src/mip/solver_settings.cu`:
- Around line 27-31: The set_mip_callback function currently pushes a nullptr
into mip_callbacks_ because it only guards the set_user_data call; update
mip_solver_settings_t<i_t, f_t>::set_mip_callback to first check if callback is
nullptr and if so do not push it (either return early or ignore the call),
otherwise call callback->set_user_data(user_data) and then push_back(callback)
so mip_callbacks_ never contains null entries; this ensures later dereferences
of entries in mip_callbacks_ (e.g., where callbacks are invoked) are safe.

In `@python/cuopt/cuopt/linear_programming/internals/internals.pyx`:
- Around line 70-76: The runtime AttributeError happens because _user_data is
assigned dynamically in the cdef classes; declare the attribute in both callback
extension types (e.g., in the class body of GetSolutionCallback and
SetSolutionCallback) as a Cython Python-object attribute like "cdef object
_user_data" (or "cdef public object _user_data" if you need external access),
then keep the existing __init__ assignment and the get_user_data_ptr cast that
uses self._user_data; ensure the declaration appears in both classes so lines
assigning/reading _user_data no longer raise.
🧹 Nitpick comments (1)
cpp/include/cuopt/linear_programming/solver_settings.hpp (1)

84-85: Document user_data ownership/lifetime in the public API.

Adding a brief Doxygen note will clarify that the pointer must remain valid for the duration of callback usage and that it’s passed through verbatim.

✍️ Suggested doc addition
-  void set_mip_callback(internals::base_solution_callback_t* callback = nullptr,
-                        void* user_data                               = nullptr);
+  /**
+   * Register a MIP solution callback.
+   * `@param` callback Callback instance (must outlive solver usage).
+   * `@param` user_data Opaque pointer passed back to the callback; must remain valid
+   *                  while the callback can be invoked.
+   */
+  void set_mip_callback(internals::base_solution_callback_t* callback = nullptr,
+                        void* user_data                               = nullptr);

As per coding guidelines, public headers should keep docs in sync with API changes.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 943b632 and 41a3b68.

📒 Files selected for processing (16)
  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
  • cpp/src/linear_programming/cuopt_c.cpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/src/mip/diversity/population.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
  • cpp/tests/mip/incumbent_callback_test.cu
  • python/cuopt/cuopt/linear_programming/internals/internals.pyx
  • python/cuopt/cuopt/linear_programming/solver/solver.pxd
  • python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx
  • python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/linear_programming/cuopt_c.cpp
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{cu,cuh,cpp,hpp,h}

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

**/*.{cu,cuh,cpp,hpp,h}: Track GPU device memory allocations and deallocations to prevent memory leaks; ensure cudaMalloc/cudaFree balance and cleanup of streams/events
Validate algorithm correctness in optimization logic: simplex pivots, branch-and-bound decisions, routing heuristics, and constraint/objective handling must produce correct results
Check numerical stability: prevent overflow/underflow, precision loss, division by zero/near-zero, and use epsilon comparisons for floating-point equality checks
Validate correct initialization of variable bounds, constraint coefficients, and algorithm state before solving; ensure reset when transitioning between algorithm phases (presolve, simplex, diving, crossover)
Ensure variables and constraints are accessed from the correct problem context (original vs presolve vs folded vs postsolve); verify index mapping consistency across problem transformations
For concurrent CUDA operations (barriers, async operations), explicitly create and manage dedicated streams instead of reusing the default stream; document stream lifecycle
Eliminate unnecessary host-device synchronization (cudaDeviceSynchronize) in hot paths that blocks GPU pipeline; use streams and events for async execution
Assess algorithmic complexity for large-scale problems (millions of variables/constraints); ensure O(n log n) or better complexity, not O(n²) or worse
Verify correct problem size checks before expensive GPU/CPU operations; prevent resource exhaustion on oversized problems
Identify assertions with overly strict numerical tolerances that fail on legitimate degenerate/edge cases (near-zero pivots, singular matrices, empty problems)
Ensure race conditions are absent in multi-GPU code and multi-threaded server implementations; verify proper synchronization of shared state
Refactor code duplication in solver components (3+ occurrences) into shared utilities; for GPU kernels, use templated device functions to avoid duplication
Check that hard-coded GPU de...

Files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
**/*.{h,hpp,py}

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

Verify C API does not break ABI stability (no struct layout changes, field reordering); maintain backward compatibility in Python and server APIs with deprecation warnings

Files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
**/*.{cpp,hpp,h}

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

**/*.{cpp,hpp,h}: Check for unclosed file handles when reading MPS/QPS problem files; ensure RAII patterns or proper cleanup in exception paths
Validate input sanitization to prevent buffer overflows and resource exhaustion attacks; avoid unsafe deserialization of problem files
Prevent thread-unsafe use of global and static variables; use proper mutex/synchronization in server code accessing shared solver state

Files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
**/*.{cu,cpp,hpp,h}

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

Avoid inappropriate use of exceptions in performance-critical GPU operation paths; prefer error codes or CUDA error checking for latency-sensitive code

Files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
cpp/include/cuopt/**/*

⚙️ CodeRabbit configuration file

cpp/include/cuopt/**/*: For public header files (C++ API):

  • Check if new public functions/classes have documentation comments (Doxygen format)
  • Flag API changes that may need corresponding docs/ updates
  • Verify parameter descriptions match actual types/behavior
  • Suggest documenting thread-safety, GPU requirements, and numerical behavior
  • For breaking changes, recommend updating docs and migration guides

Files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/include/cuopt/linear_programming/utilities/internals.hpp
**/*.{cu,cuh}

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

**/*.{cu,cuh}: Every CUDA kernel launch and memory operation must have error checking with CUDA_CHECK or equivalent verification
Avoid reinventing functionality already available in Thrust, CCCL, or RMM libraries; prefer standard library utilities over custom implementations

Files:

  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/src/mip/diversity/population.cu
  • cpp/src/mip/diversity/diversity_manager.cu
**/*.cu

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

**/*.cu: Verify race conditions and correctness of GPU kernel shared memory, atomics, and warp-level operations
Detect inefficient GPU kernel launches with low occupancy or poor memory access patterns; optimize for coalesced memory access and minimize warp divergence in hot paths

Files:

  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/src/mip/diversity/population.cu
  • cpp/src/mip/diversity/diversity_manager.cu
**/*test*.{cpp,cu,py}

📄 CodeRabbit inference engine (.github/.coderabbit_review_guide.md)

**/*test*.{cpp,cu,py}: Write tests validating numerical correctness of optimization results (not just 'runs without error'); test degenerate cases (infeasible, unbounded, empty, singleton problems)
Ensure test isolation: prevent GPU state, cached memory, and global variables from leaking between test cases; verify each test independently initializes its environment
Add tests for algorithm phase transitions: verify correct initialization of bounds and state when transitioning from presolve to simplex to diving to crossover
Add tests for problem transformations: verify correctness of original→transformed→postsolve mappings and index consistency across problem representations
Test with free variables, singleton problems, and extreme problem dimensions near resource limits to validate edge case handling

Files:

  • cpp/tests/mip/incumbent_callback_test.cu
🧠 Learnings (23)
📚 Learning: 2025-10-22T14:25:22.899Z
Learnt from: aliceb-nv
Repo: NVIDIA/cuopt PR: 527
File: cpp/src/mip/diversity/lns/rins.cu:167-175
Timestamp: 2025-10-22T14:25:22.899Z
Learning: In MIP (Mixed Integer Programming) problems in the cuOPT codebase, `n_integer_vars == 0` is impossible by definition—MIP problems must have at least one integer variable. If there are no integer variables, it would be a pure Linear Programming (LP) problem, not a MIP problem.

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Validate algorithm correctness in optimization logic: simplex pivots, branch-and-bound decisions, routing heuristics, and constraint/objective handling must produce correct results

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Check that hard-coded GPU device IDs and resource limits are made configurable; abstract multi-backend support for different CUDA versions

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Track GPU device memory allocations and deallocations to prevent memory leaks; ensure cudaMalloc/cudaFree balance and cleanup of streams/events

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Ensure race conditions are absent in multi-GPU code and multi-threaded server implementations; verify proper synchronization of shared state

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cpp,hpp,h} : Avoid inappropriate use of exceptions in performance-critical GPU operation paths; prefer error codes or CUDA error checking for latency-sensitive code

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Refactor code duplication in solver components (3+ occurrences) into shared utilities; for GPU kernels, use templated device functions to avoid duplication

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Verify error propagation from CUDA to user-facing APIs is complete; ensure CUDA errors are caught and mapped to meaningful user error codes

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.cu : Verify race conditions and correctness of GPU kernel shared memory, atomics, and warp-level operations

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Verify correct problem size checks before expensive GPU/CPU operations; prevent resource exhaustion on oversized problems

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Eliminate unnecessary host-device synchronization (cudaDeviceSynchronize) in hot paths that blocks GPU pipeline; use streams and events for async execution

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
  • cpp/src/mip/diversity/diversity_manager.cu
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : For concurrent CUDA operations (barriers, async operations), explicitly create and manage dedicated streams instead of reusing the default stream; document stream lifecycle

Applied to files:

  • cpp/include/cuopt/linear_programming/cuopt_c.h
  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/src/mip/solver_settings.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*test*.{cpp,cu,py} : Ensure test isolation: prevent GPU state, cached memory, and global variables from leaking between test cases; verify each test independently initializes its environment

Applied to files:

  • cpp/include/cuopt/linear_programming/solver_settings.hpp
  • cpp/src/mip/solver_settings.cu
  • cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
  • cpp/src/mip/diversity/population.cu
  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Validate correct initialization of variable bounds, constraint coefficients, and algorithm state before solving; ensure reset when transitioning between algorithm phases (presolve, simplex, diving, crossover)

Applied to files:

  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2026-01-15T20:41:51.616Z
Learnt from: chris-maes
Repo: NVIDIA/cuopt PR: 697
File: cpp/src/dual_simplex/diving_heuristics.cpp:200-228
Timestamp: 2026-01-15T20:41:51.616Z
Learning: In cpp/src/dual_simplex/diving_heuristics.cpp, the calculate_variable_locks function incorrectly indexes lp_problem.lower[nz_row] and lp_problem.upper[nz_row] where nz_row is a row index (0 to num_rows-1), but lower and upper are variable bounds of size num_cols indexed by column. This causes semantic errors and potential out-of-bounds access when num_rows > num_cols. Since lp_problem_t constraints are always equalities (A*x = rhs) after presolve, lock counting should treat each nonzero as contributing equally to both up_lock and down_lock, rather than checking constraint bounds.

Applied to files:

  • cpp/src/mip/diversity/diversity_manager.cu
📚 Learning: 2025-12-04T20:09:09.264Z
Learnt from: chris-maes
Repo: NVIDIA/cuopt PR: 602
File: cpp/src/linear_programming/solve.cu:732-742
Timestamp: 2025-12-04T20:09:09.264Z
Learning: In cpp/src/linear_programming/solve.cu, the barrier solver does not currently return INFEASIBLE or UNBOUNDED status. It only returns OPTIMAL, TIME_LIMIT, NUMERICAL_ISSUES, or CONCURRENT_LIMIT.

Applied to files:

  • cpp/src/mip/diversity/diversity_manager.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*test*.{cpp,cu,py} : Add tests for algorithm phase transitions: verify correct initialization of bounds and state when transitioning from presolve to simplex to diving to crossover

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*test*.{cpp,cu,py} : Add tests for problem transformations: verify correctness of original→transformed→postsolve mappings and index consistency across problem representations

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*test*.{cpp,cu,py} : Test with free variables, singleton problems, and extreme problem dimensions near resource limits to validate edge case handling

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*test*.{cpp,cu,py} : Write tests validating numerical correctness of optimization results (not just 'runs without error'); test degenerate cases (infeasible, unbounded, empty, singleton problems)

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-11-25T10:20:49.822Z
Learnt from: CR
Repo: NVIDIA/cuopt PR: 0
File: .github/.coderabbit_review_guide.md:0-0
Timestamp: 2025-11-25T10:20:49.822Z
Learning: Applies to **/*.{cu,cuh,cpp,hpp,h} : Ensure variables and constraints are accessed from the correct problem context (original vs presolve vs folded vs postsolve); verify index mapping consistency across problem transformations

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-12-06T00:22:48.638Z
Learnt from: chris-maes
Repo: NVIDIA/cuopt PR: 500
File: cpp/tests/linear_programming/c_api_tests/c_api_test.c:1033-1048
Timestamp: 2025-12-06T00:22:48.638Z
Learning: In cuOPT's quadratic programming API, when a user provides a quadratic objective matrix Q via set_quadratic_objective_matrix or the C API functions cuOptCreateQuadraticProblem/cuOptCreateQuadraticRangedProblem, the API internally computes Q_symmetric = Q + Q^T and the barrier solver uses 0.5 * x^T * Q_symmetric * x. From the user's perspective, the convention is x^T Q x. For a diagonal Q with values [q1, q2, ...], the resulting quadratic terms are q1*x1^2 + q2*x2^2 + ...

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
📚 Learning: 2025-12-04T04:11:12.640Z
Learnt from: chris-maes
Repo: NVIDIA/cuopt PR: 500
File: cpp/src/dual_simplex/scaling.cpp:68-76
Timestamp: 2025-12-04T04:11:12.640Z
Learning: In the cuOPT dual simplex solver, CSR/CSC matrices (including the quadratic objective matrix Q) are required to have valid dimensions and indices by construction. Runtime bounds checking in performance-critical paths like matrix scaling is avoided to prevent slowdowns. Validation is performed via debug-only check_matrix() calls wrapped in `#ifdef` CHECK_MATRIX.

Applied to files:

  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
🧬 Code graph analysis (7)
cpp/include/cuopt/linear_programming/cuopt_c.h (1)
cpp/src/linear_programming/cuopt_c.cpp (4)
  • cuOptSetMipGetSolutionCallback (753-764)
  • cuOptSetMipGetSolutionCallback (753-755)
  • cuOptSetMipSetSolutionCallback (766-777)
  • cuOptSetMipSetSolutionCallback (766-768)
cpp/include/cuopt/linear_programming/solver_settings.hpp (2)
cpp/include/cuopt/linear_programming/mip/solver_settings.hpp (1)
  • callback (40-41)
cpp/include/cuopt/linear_programming/utilities/internals.hpp (1)
  • user_data (35-35)
cpp/src/math_optimization/solver_settings.cu (5)
cpp/src/mip/solver_settings.cu (2)
  • set_mip_callback (27-32)
  • set_mip_callback (27-28)
python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py (1)
  • set_mip_callback (248-305)
cpp/include/cuopt/linear_programming/mip/solver_settings.hpp (1)
  • callback (40-41)
cpp/include/cuopt/linear_programming/solver_settings.hpp (1)
  • callback (84-85)
cpp/include/cuopt/linear_programming/utilities/internals.hpp (1)
  • user_data (35-35)
cpp/src/mip/solver_settings.cu (3)
cpp/include/cuopt/linear_programming/mip/solver_settings.hpp (1)
  • callback (40-41)
cpp/include/cuopt/linear_programming/solver_settings.hpp (1)
  • callback (84-85)
cpp/include/cuopt/linear_programming/utilities/internals.hpp (1)
  • user_data (35-35)
cpp/tests/mip/incumbent_callback_test.cu (3)
cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp (12)
  • data (20-29)
  • data (20-20)
  • data (31-39)
  • data (31-31)
  • data (41-56)
  • data (41-41)
  • data (63-72)
  • data (63-63)
  • data (74-82)
  • data (74-74)
  • data (84-99)
  • data (84-84)
cpp/include/cuopt/linear_programming/utilities/internals.hpp (3)
  • data (47-47)
  • data (56-56)
  • user_data (35-35)
cpp/src/linear_programming/cuopt_c.cpp (2)
  • data (57-63)
  • data (57-57)
cpp/include/cuopt/linear_programming/utilities/internals.hpp (2)
cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp (12)
  • data (20-29)
  • data (20-20)
  • data (31-39)
  • data (31-31)
  • data (41-56)
  • data (41-41)
  • data (63-72)
  • data (63-63)
  • data (74-82)
  • data (74-74)
  • data (84-99)
  • data (84-84)
cpp/src/linear_programming/cuopt_c.cpp (4)
  • data (57-63)
  • data (57-57)
  • data (73-78)
  • data (73-73)
cpp/tests/linear_programming/c_api_tests/c_api_test.c (1)
cpp/src/linear_programming/cuopt_c.cpp (16)
  • cuOptCreateProblem (140-195)
  • cuOptCreateProblem (140-153)
  • cuOptCreateSolverSettings (619-625)
  • cuOptCreateSolverSettings (619-619)
  • cuOptSetMipGetSolutionCallback (753-764)
  • cuOptSetMipGetSolutionCallback (753-755)
  • cuOptSetMipSetSolutionCallback (766-777)
  • cuOptSetMipSetSolutionCallback (766-768)
  • cuOptSolve (792-840)
  • cuOptSolve (792-794)
  • cuOptDestroyProblem (387-393)
  • cuOptDestroyProblem (387-387)
  • cuOptDestroySolverSettings (627-632)
  • cuOptDestroySolverSettings (627-627)
  • cuOptDestroySolution (842-861)
  • cuOptDestroySolution (842-842)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.10, arm64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.12, amd64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.11, arm64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.12, arm64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.13, amd64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.13, arm64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.11, amd64, rockylinux8
  • GitHub Check: wheel-build-cuopt-mps-parser / 13.1.0, 3.10, amd64, rockylinux8
  • GitHub Check: wheel-build-cuopt-sh-client / 13.1.0, 3.10, amd64, rockylinux8
🔇 Additional comments (16)
python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx (1)

185-198: LGTM — user_data is forwarded to the native callback.

Nice, this preserves backward compatibility while wiring the user context into set_mip_callback.

python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py (1)

248-305: LGTM — user_data is preserved on the callback.

This keeps the user context alive and aligns with the new callback plumbing.

cpp/src/math_optimization/solver_settings.cu (2)

3-3: No review comment needed.


385-388: LGTM: user_data is forwarded to MIP settings.

Clean passthrough that aligns with the new callback API.

cpp/include/cuopt/linear_programming/solver_settings.hpp (1)

3-3: No review comment needed.

cpp/src/mip/solver_settings.cu (1)

3-3: No review comment needed.

python/cuopt/cuopt/linear_programming/solver/solver.pxd (2)

1-1: No review comment needed.


80-82: LGTM: Cython declaration matches the updated C++ API.

The user_data parameter is correctly surfaced here.

cpp/src/mip/diversity/population.cu (2)

3-3: No review comment needed.


302-303: LGTM: user_data is forwarded to both GET and SET callbacks.

This keeps the callback context intact through the MIP solution pipeline.

Also applies to: 324-326

python/cuopt/cuopt/linear_programming/internals/internals.pyx (1)

24-32: Signature alignment with native callbacks looks correct.

cpp/tests/mip/incumbent_callback_test.cu (2)

43-45: LGTM for the updated set_solution signature.


68-70: LGTM for the updated get_solution signature.

cpp/tests/linear_programming/c_api_tests/c_api_test.c (2)

12-12: No issues with the CUDA runtime include.


135-296: Good callback test coverage and cleanup flow.

cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp (1)

41-55: Backward-compatible user_data handling looks good.

Also applies to: 84-98

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@python/cuopt/cuopt/linear_programming/internals/internals.pyx`:
- Around line 86-97: The _user_data attribute and get_user_data_ptr() are
duplicated and lack a setter in GetSolutionCallback/SetSolutionCallback; move
_user_data, its initialization and the get_user_data_ptr() implementation into
the PyCallback base class (add initialization in PyCallback.__init__ and expose
a setter there), remove the duplicated _user_data and get_user_data_ptr() from
GetSolutionCallback and SetSolutionCallback, and ensure subclasses call
super().__init__() (or otherwise inherit) so the native_callback.pyCallbackClass
assignment in their __init__ still references the correct self.
- Around line 66-77: The Cython cdef attribute _user_data is currently C-level
only so Python assigns a shadow attribute (making user_data passed to
set_mip_callback ignored); expose it to Python by adding a Python-accessible
property or declaring it cdef public (e.g., provide a user_data property with
getter/setter that stores the C-level _user_data and returns/accepts a Python
object) in the cdef class (adjust __init__ to use the new property if needed),
ensure get_user_data_ptr() reads the same backing field, and update caller sites
(solver_settings.set_mip_callback and the analogous SetSolutionCallback usage)
to set callback.user_data = user_data instead of callback._user_data = user_data
so the Python-assigned value is visible to get_user_data_ptr().
🧹 Nitpick comments (1)
python/cuopt/cuopt/linear_programming/internals/internals.pyx (1)

72-77: Document the lifetime contract for the returned pointer.

The returned uintptr_t is a borrowed reference — no Py_INCREF is performed. The pointer remains valid only while self (the callback object) is alive. If C code caches this pointer beyond the callback's lifetime, it risks use-after-free.

Consider adding a docstring clarifying that the caller must keep the callback object alive while using the pointer:

cpdef uintptr_t get_user_data_ptr(self):
    """Return raw pointer to user_data. Valid only while this callback is alive."""
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fda3f4f and 4e50db4.

📒 Files selected for processing (2)
  • python/cuopt/cuopt/linear_programming/internals/internals.pyx
  • python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx
🔇 Additional comments (2)
python/cuopt/cuopt/linear_programming/internals/internals.pyx (2)

11-11: LGTM!

Using cpython.ref cimport PyObject is the idiomatic Cython approach, cleaner than a manual extern declaration.


21-29: LGTM!

The extern declarations correctly extend the callback signatures to include the user_data parameter, enabling per-callback user context propagation.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

/**
* @brief Callback for injecting MIP solutions with user context.
*
* @param[out] solution - Device pointer to solution values to set.
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably should mention the dimension of the solution_values (i.e. that it needs to be equal to the number of variables in the original problem).

Also, do we crush the solution from the original variables into the presolve variables when MIP presolve is turned on?

Copy link
Contributor

Choose a reason for hiding this comment

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

And this should probably be a host pointer rather than a device pointer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@chris-maes Good catch! All callbacks are disabled if 3rd party presolve(Papilo) is enabled. Our own presolve changes are handled though. I believe this is a blocker for primal integral experiments. I will give it a try.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have enabled third party presolve and handled crushing/uncrushing of solutions.

Comment on lines 687 to 688
* @param[in] solution - Device pointer to incumbent solution values.
* @param[in] objective_value - Device pointer to incumbent objective value.
Copy link
Contributor

@aliceb-nv aliceb-nv Jan 21, 2026

Choose a reason for hiding this comment

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

I wonder if these should be host pointers instead, in terms of API design. So far the API has been abstracting away the reality of the CUDA implementation (such as the corresponding context, the streams used...) away from the user, so this may represent a break in the philosophy. Plus we don't expose the GPU problem structure either, so it is likely the user will have to copy it back to the host either way to do any useful work
Maybe device-space callbacks could be implemented later on if need be? We might have to discuss this

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. The rest of the API uses abstracts out the device. Probably we should provide a host pointer here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are agnostic to device/host memory in all APIs AFAIK. I can modify it to "Host/Device" pointer? The only caveat of supporting device pointers officialy is that the copies will be done by internal stream.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made all callbacks to input/output host pointers.

@rgsl888prabhu rgsl888prabhu changed the base branch from main to release/26.02 January 22, 2026 16:39
Copy link
Collaborator

@rgsl888prabhu rgsl888prabhu left a comment

Choose a reason for hiding this comment

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

Lets add a python test to verify the new changes. Also please update documentation and add an example of this call back in docs/cuopt/source

@akifcorduk akifcorduk requested a review from a team as a code owner January 27, 2026 12:40
@akifcorduk akifcorduk requested a review from gforsyth January 27, 2026 12:40
@akifcorduk
Copy link
Contributor Author

Lets add a python test to verify the new changes. Also please update documentation and add an example of this call back in docs/cuopt/source

@rgsl888prabhu the callback structure is mostly the same. What changed is we changed pointers of callbacks from device to host. Also an additional parameter user_data is added. These are reflected in python tests, examples and docs. (I didn't write a new one)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (3)
cpp/src/mip/solve.cu (1)

190-224: Presolve rebuild should preserve user tolerances.
Reconstructing the reduced problem without settings.get_tolerances() can reset integrality/feasibility tolerances to defaults, changing behavior for users who customize tolerances.

🔧 Proposed fix
-      problem = detail::problem_t<i_t, f_t>(presolve_result->reduced_problem);
+      problem = detail::problem_t<i_t, f_t>(presolve_result->reduced_problem,
+                                            settings.get_tolerances());
python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py (1)

248-310: Make user_data parameter optional to maintain backward compatibility.

The previous API signature was set_mip_callback(self, callback). This change adds user_data as a required parameter, breaking existing code. Per the coding guidelines, maintain backward compatibility with deprecation warnings by making user_data=None and warning when omitted.

♻️ Suggested compatibility patch
+import warnings
 from enum import IntEnum, auto
@@
-    def set_mip_callback(self, callback, user_data):
+    def set_mip_callback(self, callback, user_data=None):
@@
-        user_data : object
-            User context passed to the callback.
+        user_data : object, optional
+            User context passed to the callback. Passing None is deprecated.
@@
-        if callback is not None:
-            callback.user_data = user_data
+        if user_data is None:
+            warnings.warn(
+                "set_mip_callback(callback) without user_data is deprecated; "
+                "pass user_data explicitly.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+        if callback is not None:
+            callback.user_data = user_data
         self.mip_callbacks.append(callback)
cpp/src/mip/presolve/third_party_presolve.cpp (1)

29-30: Static globals create thread-safety issues in concurrent solver usage.

post_solve_storage_ and maximize_ are static globals at module scope. If multiple third_party_presolve_t instances call apply() concurrently from different threads (e.g., in a server environment solving independent problems), these statics will race. A subsequent undo() call in one thread could read incorrect postsolve data written by another thread's apply().

Convert these to instance members of third_party_presolve_t.

🤖 Fix all issues with AI agents
In `@cpp/src/mip/presolve/third_party_presolve.cpp`:
- Around line 513-527: uncrush_primal_solution currently uses the shared static
post_solve_storage_ which is not thread-safe; change the implementation to use
an instance-local copy (or guard access with a mutex) of post_solve_storage_
when constructing papilo::Postsolve<f_t> so concurrent presolvers do not race on
shared state, and add explicit handling for papilo::PostsolveStatus::kFailed
after calling post_solver.undo (e.g., throw a descriptive exception or log and
return an error) instead of only relying on check_postsolve_status; references:
third_party_presolve_t::uncrush_primal_solution, post_solve_storage_,
papilo::Postsolve::undo, and papilo::PostsolveStatus::kFailed.

In `@cpp/src/mip/problem/presolve_data.cuh`:
- Around line 78-93: Validate sizes and ranges before storing or using Papilo
presolve data: in set_papilo_presolve_data check that reduced_to_original.size()
== original_to_reduced.size() or that each mapping lies within [0,
original_num_variables) and that original_num_variables is > 0, and reject or
assert/log + do not set papilo_presolve_ptr/papilo_original_num_variables if
these checks fail; in papilo_uncrush_assignment verify the incoming
assignment.size() equals the reduced problem size expected by
papilo_presolve_ptr (or matches
original_to_reduced.size()/reduced_to_original.size() as appropriate) and
error/assert before calling the third-party uncrush_primal_solution to avoid
silent corruption. Use the existing symbols papilo_presolve_ptr,
reduced_to_original, original_to_reduced, papilo_original_num_variables,
papilo_uncrush_assignment and uncrush_primal_solution in your checks and
fail-fast when inconsistencies are detected.

In `@cpp/tests/mip/incumbent_callback_test.cu`:
- Around line 131-132: The test references nonexistent MPS files causing runtime
failure; update the test_instances vector in incumbent_callback_test.cu
(variable test_instances) to use existing dataset filenames (e.g., replace
"mip/swath1.mps" with a valid file from datasets/mip/, and if enabling the other
entries, use "50v-10-free-bound.mps" and "neos5-free-bound.mps" instead of
"mip/50v-10.mps" and "mip/neos5.mps"), or alternatively add the missing MPS
files to the repository; modify the string literals in the test_instances
initializer accordingly so they match actual files.

In `@cpp/tests/mip/mip_utils.cuh`:
- Around line 142-143: The top-level std::vector<double> viol declared alongside
residual is unused and is later shadowed by a loop variable named viol; remove
the outer declaration of viol (the std::vector<double> viol that parallels
residual and uses constraint_lower_bounds.size()) to eliminate dead code and the
shadowing issue, and apply the same removal to the corresponding unused viol
declaration in the other overload (the one at the earlier occurrence referenced
in the review).
🧹 Nitpick comments (8)
cpp/tests/linear_programming/utilities/pdlp_test_utilities.cuh (1)

59-81: Consider extracting shared logic to reduce duplication.

The new overload is correct. Both overloads share identical core logic (size check, transform with std::multiplies, reduce, EXPECT_NEAR). You could extract this into a helper that accepts const std::vector<double>&, then have the device_uvector overload call it after host_copy.

♻️ Optional refactor to consolidate logic
+// Helper for objective sanity check on host vectors
+static void test_objective_sanity_impl(
+  const std::vector<double>& primal_vars,
+  const std::vector<double>& c_vector,
+  double objective_value,
+  double epsilon)
+{
+  if (primal_vars.size() != c_vector.size()) {
+    EXPECT_EQ(primal_vars.size(), c_vector.size());
+    return;
+  }
+  std::vector<double> out(primal_vars.size());
+  std::transform(primal_vars.cbegin(),
+                 primal_vars.cend(),
+                 c_vector.cbegin(),
+                 out.begin(),
+                 std::multiplies<double>());
+  double sum = std::reduce(out.cbegin(), out.cend(), 0.0);
+  EXPECT_NEAR(sum, objective_value, epsilon);
+}
+
 // Compute on the CPU x * c to check that the returned objective value is correct
 static void test_objective_sanity(
   const cuopt::mps_parser::mps_data_model_t<int, double>& op_problem,
   const rmm::device_uvector<double>& primal_solution,
   double objective_value,
   double epsilon = tolerance)
 {
   const auto primal_vars = host_copy(primal_solution, primal_solution.stream());
-  const auto& c_vector   = op_problem.get_objective_coefficients();
-  if (primal_vars.size() != c_vector.size()) {
-    EXPECT_EQ(primal_vars.size(), c_vector.size());
-    return;
-  }
-  std::vector<double> out(primal_vars.size());
-  std::transform(primal_vars.cbegin(),
-                 primal_vars.cend(),
-                 c_vector.cbegin(),
-                 out.begin(),
-                 std::multiplies<double>());
-
-  double sum = std::reduce(out.cbegin(), out.cend(), 0.0);
-
-  EXPECT_NEAR(sum, objective_value, epsilon);
+  test_objective_sanity_impl(primal_vars, op_problem.get_objective_coefficients(), objective_value, epsilon);
 }

 // Compute on the CPU x * c to check that the returned objective value is correct
 static void test_objective_sanity(
   const cuopt::mps_parser::mps_data_model_t<int, double>& op_problem,
   const std::vector<double>& primal_solution,
   double objective_value,
   double epsilon = tolerance)
 {
-  const auto& c_vector = op_problem.get_objective_coefficients();
-  if (primal_solution.size() != c_vector.size()) {
-    EXPECT_EQ(primal_solution.size(), c_vector.size());
-    return;
-  }
-  std::vector<double> out(primal_solution.size());
-  std::transform(primal_solution.cbegin(),
-                 primal_solution.cend(),
-                 c_vector.cbegin(),
-                 out.begin(),
-                 std::multiplies<double>());
-
-  double sum = std::reduce(out.cbegin(), out.cend(), 0.0);
-
-  EXPECT_NEAR(sum, objective_value, epsilon);
+  test_objective_sanity_impl(primal_solution, op_problem.get_objective_coefficients(), objective_value, epsilon);
 }
cpp/src/dual_simplex/branch_and_bound.cpp (1)

326-351: Avoid unconditional printf under mutex; use debug logger after unlock.

set_new_solution is a hot path and can be called frequently; the unconditional printf inside the lock can flood logs and add contention. Consider switching to log.debug and emitting after releasing mutex_upper_.

♻️ Proposed refactor
-  mutex_upper_.lock();
-  if (obj < upper_bound_) {
+  bool better_than_upper = false;
+  mutex_upper_.lock();
+  if (obj < upper_bound_) {
+    better_than_upper = true;
     f_t primal_err;
     f_t bound_err;
     i_t num_fractional;
     is_feasible = check_guess(
       original_lp_, settings_, var_types_, crushed_solution, primal_err, bound_err, num_fractional);
     if (is_feasible) {
       upper_bound_ = obj;
       incumbent_.set_incumbent_solution(obj, crushed_solution);
     } else {
       attempt_repair         = true;
       constexpr bool verbose = false;
       if (verbose) {
         settings_.log.printf(
           "Injected solution infeasible. Constraint error %e bound error %e integer infeasible "
           "%d\n",
           primal_err,
           bound_err,
           num_fractional);
       }
     }
-  } else {
-    settings_.log.printf(
-      "[DEBUG] set_new_solution: Solution objective not better than current upper_bound_. Not "
-      "accepted.\n");
   }
   mutex_upper_.unlock();
+  if (!better_than_upper) {
+    settings_.log.debug(
+      "set_new_solution: solution objective not better than current upper_bound_; not accepted.");
+  }
python/cuopt_server/cuopt_server/utils/linear_programming/solver.py (1)

81-88: Consider replacing assertion with explicit error handling for production robustness.

Using assert for runtime validation in a server context can be problematic—assertions can be disabled with -O flag, and AssertionError may not provide meaningful diagnostics to API consumers. Consider raising a more specific exception.

♻️ Suggested improvement
     def get_solution(self, solution, solution_cost, user_data):
-        if user_data is not None:
-            assert user_data == self.req_id
+        if user_data is not None and user_data != self.req_id:
+            raise ValueError(
+                f"Callback user_data mismatch: expected {self.req_id}, got {user_data}"
+            )
         self.sender(
             self.req_id,
             solution.tolist(),
             float(solution_cost[0]),
         )
cpp/tests/mip/mip_utils.cuh (1)

45-70: Consider extracting common validation logic to reduce duplication.

This overload duplicates the validation logic from the device-vector version (lines 17-43). While acceptable for test utilities, a templated helper or a version that operates on raw const double* could serve both callers and reduce maintenance burden.

♻️ Example refactor approach
// Internal helper that works with raw pointers
static void test_variable_bounds_impl(
  const double* lower_bound_ptr,
  const double* upper_bound_ptr,
  const double* assignment_ptr,
  std::size_t size,
  double tolerance)
{
  std::vector<int> indices(size);
  std::iota(indices.begin(), indices.end(), 0);
  bool result = std::all_of(indices.begin(), indices.end(), [=](int idx) {
    bool res = true;
    if (lower_bound_ptr != nullptr) {
      res = res && (assignment_ptr[idx] >= lower_bound_ptr[idx] - tolerance);
    }
    if (upper_bound_ptr != nullptr) {
      res = res && (assignment_ptr[idx] <= upper_bound_ptr[idx] + tolerance);
    }
    return res;
  });
  EXPECT_TRUE(result);
}

// Then both overloads call the impl
cpp/tests/linear_programming/c_api_tests/c_api_test.c (1)

180-279: Add a data-integrity assertion for callbacks.
Right now the test only checks call counts and allocation errors. Consider validating that the objective seen in the get-solution callback matches the solver’s objective to confirm correct data transfer. As per coding guidelines.

✅ Suggested enhancement
   if (context.get_calls < 1 || context.set_calls < 1) {
     printf("Expected callbacks to be called at least once\n");
     status = CUOPT_INVALID_ARGUMENT;
     goto DONE;
   }
+
+  cuopt_float_t objective_value = 0;
+  status = cuOptGetObjectiveValue(solution, &objective_value);
+  if (status != CUOPT_SUCCESS) {
+    printf("Error getting objective value\n");
+    goto DONE;
+  }
+  if (objective_value != context.last_objective) {
+    printf("Callback objective mismatch\n");
+    status = CUOPT_INVALID_ARGUMENT;
+    goto DONE;
+  }
cpp/src/mip/diversity/population.cu (1)

321-352: Verify size consistency when papilo presolve resizes the problem.

The incumbent_assignment is allocated with callback_num_variables (either original problem size or papilo original size), but the assertion on line 356 checks against outside_sol.assignment.size(). After papilo_crush_assignment (line 350), the incumbent_assignment will be resized to the reduced problem size, which should match outside_sol.assignment.size().

However, consider adding a comment clarifying this flow, as the size of incumbent_assignment changes after crushing.

📝 Suggested documentation improvement
       if (problem_ptr->has_papilo_presolve_data()) {
         problem_ptr->papilo_crush_assignment(incumbent_assignment);
       }
+      // Note: incumbent_assignment is now resized to the reduced (presolved) problem size

       if (context.settings.mip_scaling) { context.scaling.scale_solutions(incumbent_assignment); }
cpp/src/mip/problem/presolve_data.cu (2)

119-132: Host-side loop for free variable handling could be optimized.

Lines 123-128 iterate over the assignment on the host to undo the free variable decomposition. This requires a device-to-host copy, host computation, and host-to-device copy. For large problems, this could be a performance bottleneck.

Consider keeping this on the GPU with a thrust kernel similar to the forward transformation in pre_process_assignment.

♻️ GPU-based alternative
-  auto h_assignment = cuopt::host_copy(current_assignment, problem.handle_ptr->get_stream());
-  cuopt_assert(additional_var_id_per_var.size() == h_assignment.size(), "Size mismatch");
-  cuopt_assert(additional_var_used.size() == h_assignment.size(), "Size mismatch");
-  for (i_t i = 0; i < (i_t)h_assignment.size(); ++i) {
-    if (additional_var_used[i]) {
-      cuopt_assert(additional_var_id_per_var[i] != -1, "additional_var_id_per_var is not set");
-      h_assignment[i] -= h_assignment[additional_var_id_per_var[i]];
-    }
-  }
-  raft::copy(current_assignment.data(),
-             h_assignment.data(),
-             h_assignment.size(),
-             problem.handle_ptr->get_stream());
+  auto d_additional_var_used =
+    cuopt::device_copy(additional_var_used, problem.handle_ptr->get_stream());
+  auto d_additional_var_id_per_var =
+    cuopt::device_copy(additional_var_id_per_var, problem.handle_ptr->get_stream());
+  thrust::for_each(
+    problem.handle_ptr->get_thrust_policy(),
+    thrust::make_counting_iterator<i_t>(0),
+    thrust::make_counting_iterator<i_t>(current_assignment.size()),
+    [additional_var_used       = d_additional_var_used.data(),
+     additional_var_id_per_var = d_additional_var_id_per_var.data(),
+     assgn                     = current_assignment.data()] __device__(auto idx) {
+      if (additional_var_used[idx]) {
+        assgn[idx] -= assgn[additional_var_id_per_var[idx]];
+      }
+    });

181-203: Bounds check silently sets value to zero for invalid indices.

Lines 191-194 set reduced_assignment[i] = f_t{0} when original_idx is out of range. This silent fallback could mask bugs in the mapping construction. Consider logging a warning or using an assertion in debug builds.

📝 Suggested improvement
     if (original_idx < 0 || static_cast<size_t>(original_idx) >= h_assignment.size()) {
+      CUOPT_LOG_DEBUG("papilo_crush_assignment: invalid original_idx %d at reduced index %zu",
+                      original_idx, i);
       reduced_assignment[i] = f_t{0};
       continue;
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@cpp/tests/mip/mip_utils.cuh`:
- Around line 130-159: Add a defensive size check before the CSR SpMV in
test_constraint_sanity_per_row: compute the maximum index in indices (e.g., via
std::max_element) and assert with cuopt_assert that solution.size() > max_index
to prevent out-of-bounds access when indexing solution[indices[j]]; update the
test_constraint_sanity_per_row function to perform this check (and handle the
case of empty indices/offsets appropriately) before entering the nested loops.
🧹 Nitpick comments (2)
cpp/tests/linear_programming/utilities/pdlp_test_utilities.cuh (1)

59-81: Implementation is correct; consider extracting common logic to reduce duplication.

The new overload correctly validates the objective value computation. However, it duplicates nearly all logic from the rmm::device_uvector overload (lines 35-57).

♻️ Optional: Extract common logic into a helper
+// Helper to compute and verify objective from iterators
+template <typename Iter>
+static void verify_objective_value(
+  Iter primal_begin,
+  Iter primal_end,
+  const std::vector<double>& c_vector,
+  double objective_value,
+  double epsilon)
+{
+  std::vector<double> out(std::distance(primal_begin, primal_end));
+  std::transform(primal_begin, primal_end, c_vector.cbegin(), out.begin(), std::multiplies<double>());
+  double sum = std::reduce(out.cbegin(), out.cend(), 0.0);
+  EXPECT_NEAR(sum, objective_value, epsilon);
+}
+
 // Compute on the CPU x * c to check that the returned objective value is correct
 static void test_objective_sanity(
   const cuopt::mps_parser::mps_data_model_t<int, double>& op_problem,
   const rmm::device_uvector<double>& primal_solution,
   double objective_value,
   double epsilon = tolerance)
 {
   const auto primal_vars = host_copy(primal_solution, primal_solution.stream());
   const auto& c_vector   = op_problem.get_objective_coefficients();
   if (primal_vars.size() != c_vector.size()) {
     EXPECT_EQ(primal_vars.size(), c_vector.size());
     return;
   }
-  std::vector<double> out(primal_vars.size());
-  std::transform(primal_vars.cbegin(),
-                 primal_vars.cend(),
-                 c_vector.cbegin(),
-                 out.begin(),
-                 std::multiplies<double>());
-
-  double sum = std::reduce(out.cbegin(), out.cend(), 0.0);
-
-  EXPECT_NEAR(sum, objective_value, epsilon);
+  verify_objective_value(primal_vars.cbegin(), primal_vars.cend(), c_vector, objective_value, epsilon);
 }

Then the std::vector overload can similarly delegate to verify_objective_value.

cpp/src/mip/problem/problem.cu (1)

1764-1806: Avoid const_cast by making papilo helpers const-correct.

The wrapper uses const_cast even though the presolve helpers appear read-only on problem. Consider changing the presolve_data signatures to take const problem_t& and update call sites so the wrapper stays const-safe.

♻️ Suggested refactor (const-correctness)
-void papilo_uncrush_assignment(problem_t<i_t, f_t>& problem,
+void papilo_uncrush_assignment(const problem_t<i_t, f_t>& problem,
                                rmm::device_uvector<f_t>& assignment) const;

-void papilo_crush_assignment(problem_t<i_t, f_t>& problem,
+void papilo_crush_assignment(const problem_t<i_t, f_t>& problem,
                              rmm::device_uvector<f_t>& assignment) const;
-void problem_t<i_t, f_t>::papilo_uncrush_assignment(rmm::device_uvector<f_t>& assignment) const
-{
-  presolve_data.papilo_uncrush_assignment(const_cast<problem_t&>(*this), assignment);
-}
+void problem_t<i_t, f_t>::papilo_uncrush_assignment(rmm::device_uvector<f_t>& assignment) const
+{
+  presolve_data.papilo_uncrush_assignment(*this, assignment);
+}

-void problem_t<i_t, f_t>::papilo_crush_assignment(rmm::device_uvector<f_t>& assignment) const
-{
-  presolve_data.papilo_crush_assignment(const_cast<problem_t&>(*this), assignment);
-}
+void problem_t<i_t, f_t>::papilo_crush_assignment(rmm::device_uvector<f_t>& assignment) const
+{
+  presolve_data.papilo_crush_assignment(*this, assignment);
+}

@rgsl888prabhu
Copy link
Collaborator

Lets add a python test to verify the new changes. Also please update documentation and add an example of this call back in docs/cuopt/source

@rgsl888prabhu the callback structure is mostly the same. What changed is we changed pointers of callbacks from device to host. Also an additional parameter user_data is added. These are reflected in python tests, examples and docs. (I didn't write a new one)

I meant testing user_data since it is a new parameter.

@akifcorduk
Copy link
Contributor Author

Lets add a python test to verify the new changes. Also please update documentation and add an example of this call back in docs/cuopt/source

@rgsl888prabhu the callback structure is mostly the same. What changed is we changed pointers of callbacks from device to host. Also an additional parameter user_data is added. These are reflected in python tests, examples and docs. (I didn't write a new one)

I meant testing user_data since it is a new parameter.

It is tested here: https://github.com/NVIDIA/cuopt/pull/779/files#diff-3db7ef937cb0c65153bfa4aa4378af5326fb873b9fdd29fb240e141b1f9c68deR43

It is enough that we prove user_data is passed back to user within callback. We don't need to do various values here.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants