Skip to content

[DRAFT] Solver persistence pipeline#1518

Open
Iroy30 wants to merge 1 commit into
NVIDIA:mainfrom
Iroy30:solver_cache_persistence_barrier
Open

[DRAFT] Solver persistence pipeline#1518
Iroy30 wants to merge 1 commit into
NVIDIA:mainfrom
Iroy30:solver_cache_persistence_barrier

Conversation

@Iroy30

@Iroy30 Iroy30 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Description

Issue

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

@Iroy30 Iroy30 requested review from a team as code owners July 6, 2026 05:35
@copy-pr-bot

copy-pr-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a reusable GPU barrier solve session (lp_solve_session_t) enabling cuDSS symbolic factorization and RAFT/CUDA handle reuse across repeated LP/QCQP solves when sparsity is unchanged, gated by sparsity-hash matching. Adds cache profiling instrumentation, threads the session through dual_simplex/barrier/PDLP C++ layers, exposes it via Cython/PyCapsule to Python's Problem/Solution/SolverSettings, and adds tests and benchmark scripts.

Changes

LP Solve Session Reuse Feature

Layer / File(s) Summary
Session and settings contracts
cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp, cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp, cpp/include/cuopt/linear_programming/utilities/cython_types.hpp, cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp
Declares lp_solve_session_t, adds session_enabled/lp_solve_session settings fields, extends call_solve signature, and adds session ownership to linear_programming_ret_t.
Cache profiling utility
cpp/include/cuopt/linear_programming/utilities/solver_cache_profiler.hpp
Adds cache_id enum, profiler_t singleton, scoped_timer_t RAII timer, and profiling macros gated by CUOPT_CACHE_PROFILE.
Barrier symbolic cache and sparsity hashing
cpp/src/barrier/barrier_symbolic_cache.hpp, cpp/src/barrier/barrier_factorization_sparsity_hash.*, cpp/src/barrier/CMakeLists.txt
Introduces barrier_symbolic_cache_t and FNV-1a-based sparsity hashing for host/device CSR and augmented KKT matrices.
Barrier solver symbolic reuse integration
cpp/src/barrier/barrier.cu, cpp/src/barrier/barrier.hpp, cpp/src/barrier/cusparse_view.*, cpp/src/barrier/device_sparse_matrix.cuh
Adopts/pins cached CSR matrices when sparsity hashes match, wires finish_session to store/clear symbolic cache based on solve status, and adds matrix value-update/move-semantics support.
cuDSS symbolic/numeric rebind hooks
cpp/src/barrier/sparse_cholesky.cuh
Adds invalidate_numeric_factor/rebind_csr_matrix/rebind_settings hooks, switches to pointer-based settings, and gates analyze/factorize/solve on new validity flags.
Dual simplex and PDLP solve path wiring
cpp/src/dual_simplex/solve.*, cpp/src/pdlp/solve.cu, cpp/src/pdlp/utilities/cython_solve.cu, cpp/src/pdlp/utilities/lp_solve_session.cu, cpp/src/pdlp/CMakeLists.txt
Threads session pointer through solve overloads, adds problem fingerprinting, implements lp_solve_session_t, and creates/attaches/returns sessions in call_solve.
Python/Cython session bindings
python/cuopt/cuopt/linear_programming/problem.py, solution/solution.py, solver/solver*.py(x), solver_settings/solver_settings.p*
Adds session parameter through Solve, PyCapsule wiring, session_enabled setting, and Problem.solve session reuse with vectorized slack computation.
Session cache reuse test suite
python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py, test_lp_solve_session.py
Adds helpers and pytest cases validating warm symbolic reuse and full reanalysis on structural changes.

Standalone Benchmark and Verification Scripts

Layer / File(s) Summary
Portfolio QP session performance benchmark
script_perf_eval.py
Benchmarks cold vs warm solves with/without session reuse for a portfolio QP, verifies objectives, and writes JSON artifacts.
Session cache scenario runner script
script_session_cache_tests.py
Standalone runner exercising ADAT/augmented reuse and mismatch scenarios with pass/fail reporting.

Estimated code review effort: 4 (Complex) | ~75 minutes

Suggested labels: non-breaking, improvement

Suggested reviewers: ramakrishnap-nv, chris-maes, tmckayus

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to solver persistence, but it's too generic to convey the actual session-cache changes. Rename it to something like 'Add reusable LP solve sessions and barrier cache persistence'.
Description check ❓ Inconclusive The description is mostly an empty template, but it still loosely points to the solver persistence change. Replace the template text with a brief summary of LP session reuse, cache persistence, and test coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch solver_cache_persistence_barrier

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (6)
cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp (1)

333-336: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document the pointer's lifetime/ownership contract.

lp_solve_session is a non-owning pointer set externally by call_solve; ownership of the pointee later moves into linear_programming_ret_t::lp_solve_session (a unique_ptr). The comment doesn't clarify how long this raw pointer stays valid or whether callers must reset it between logically-unrelated solves (e.g. when sparsity/structure changes). Given the PR's explicit "session reuse as a state-reset risk" concern, spelling this out here would help prevent future dangling-pointer misuse.

As per path instructions, "Suggest documenting thread-safety, GPU requirements, numerical behavior" for public C++ headers under cpp/include/cuopt/**/*.

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

In `@cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp` around lines
333 - 336, Document the ownership and lifetime contract for lp_solve_session in
solver_settings.hpp: make it explicit that this raw pointer is non-owning, is
only valid for the duration of the matching call_solve/solve flow, and that
ownership is transferred later into linear_programming_ret_t::lp_solve_session
(the unique_ptr). Also note that callers should clear or replace it before
unrelated solves or any structural/sparsity change, and add a short note on any
thread-safety expectations if session_enabled and lp_solve_session are reused
across calls.

Source: Path instructions

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

1656-1660: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant conditional — both branches are identical.

n_rows computes len(self.rhs) in both the if and else arms.

♻️ Simplify
-        n_rows = (
-            len(self.rhs)
-            if isinstance(self.rhs, np.ndarray)
-            else len(self.rhs)
-        )
+        n_rows = len(self.rhs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cuopt/cuopt/linear_programming/problem.py` around lines 1656 - 1660,
The n_rows assignment in problem.py is using a redundant isinstance(self.rhs,
np.ndarray) conditional because both branches call len(self.rhs); simplify the
expression in the same location by removing the unnecessary if/else and keeping
a single len(self.rhs) computation, preserving the existing behavior in the
problem-related method that builds the row count.
script_perf_eval.py (2)

208-218: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid the dense diagonal materialization.

np.diag(info["D_diag"]) builds an n × n dense matrix (n=5000 by default → ~200 MB) on every objective snapshot just to compute a diagonal quadratic form. Use the elementwise form instead.

♻️ Proposed change
     y = info["F"].T @ x_np
     z = np.abs(x_np - info["x0"])
-    d_matrix = np.diag(info["D_diag"])
     return (
         -info["mu"] @ x_np
         + info["gamma"]
-        * (x_np @ d_matrix @ x_np + y @ info["Omega"] @ y)
+        * (np.sum(info["D_diag"] * x_np * x_np) + y @ info["Omega"] @ y)
         + info["tc_rate"] * np.sum(z)
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script_perf_eval.py` around lines 208 - 218, The portfolio objective in
portfolio_objective is materializing a dense diagonal matrix via
np.diag(info["D_diag"]) just to compute the diagonal quadratic term. Replace the
x_np @ d_matrix @ x_np path with the equivalent elementwise formulation using
info["D_diag"] directly, and remove the d_matrix allocation so the objective
stays memory-efficient.

489-548: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused cross-mode helpers verify_objectives_across_modes, _print_objective_cross_check, and _write_results are no longer called; --mode all uses _compare_mode_objectives and _write_bench_artifact instead. Removing the stale helpers would keep script_perf_eval.py easier to follow.

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

In `@script_perf_eval.py` around lines 489 - 548, Remove the stale cross-mode
objective helpers from script_perf_eval.py: verify_objectives_across_modes,
_print_objective_cross_check, and _write_results are no longer used because
--mode all now routes through _compare_mode_objectives and
_write_bench_artifact. Delete these unused functions and any now-dead references
so the module only keeps the active benchmark flow.
cpp/src/barrier/barrier_factorization_sparsity_hash.hpp (1)

54-117: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Duplicated augmented-KKT layout logic risks silent drift from form_augmented.

This function manually reconstructs the same row/diagonal/column ordering as iteration_data_t::form_augmented(true) (per the comment on line 57), and it's the only cross-check available for the production hash path in hash_device_csr_sparsity_pattern. The cuopt_assert comparing host vs. device hashes in barrier.cu only runs under #ifndef NDEBUG, so any future divergence between this function and the real matrix-construction code would go undetected in release builds, potentially causing symbolic-cache reuse decisions to be made against a subtly wrong sparsity hash.

Consider extracting the shared column-ordering logic (diagonal insertion, Q/A/AT traversal order) into a single helper used by both form_augmented and this hash function, so they cannot diverge independently.

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

In `@cpp/src/barrier/barrier_factorization_sparsity_hash.hpp` around lines 54 -
117, The augmented-KKT sparsity layout is duplicated here and in
iteration_data_t::form_augmented(true), so the hash logic can silently drift
from the real matrix construction. Refactor the shared ordering rules (Q
diagonal handling, A row entries, and AT lower block traversal) into a common
helper and have both hash_augmented_kkt_sparsity and form_augmented use it, so
the host hash stays identical to the production layout and
hash_device_csr_sparsity_pattern checks remain trustworthy.
cpp/src/pdlp/utilities/cython_solve.cu (1)

144-170: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

ephemeral_handle is constructed even when a session handle is used, and the else-branch C01 timer measures nothing.

ephemeral_stream/ephemeral_handle are created unconditionally at Lines 144-145, but in the want_session path solve_handle is reassigned to the session handle (Line 161), leaving the freshly-constructed raft::handle_t unused — an avoidable allocation/CUDA-context setup. Also, the else-branch at Lines 164-169 records elapsed time between two immediately-adjacent steady_clock::now() calls, so the C01 sample is effectively zero and does not capture the handle-creation cost (which already happened at Line 145). Consider constructing the ephemeral handle only in the non-session branch and moving the timer around the actual construction.

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

In `@cpp/src/pdlp/utilities/cython_solve.cu` around lines 144 - 170, The setup in
cython_solve.cu is doing unnecessary work and misreporting timing:
`ephemeral_stream`/`ephemeral_handle` are created before the
`memory_backend_t::GPU` and `want_session` checks even though `solve_handle` is
switched to `active_session->handle_ptr()` in the session path, and the `else`
branch around `C01` records time without measuring any construction. Update the
control flow so `raft::handle_t` is only created in the non-session path, and
place the `cache_profile::C01` timing around the actual ephemeral handle
creation (using the existing `ephemeral_stream`, `ephemeral_handle`, and
`lp_solve_session_t::create`/`handle_ptr` logic) so the sample reflects real
setup cost.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp`:
- Around line 28-63: The public API on lp_solve_session_t is missing Doxygen and
an explicit threading contract. Add comments for create, handle_ptr,
stream_view, clear_symbolic_cache, and store_symbolic_cache describing
ownership, return values, and cache behavior. Also state clearly that
lp_solve_session_t is not thread-safe for concurrent use, and that the
unsynchronized cache mutators and reuse helpers must only be called from a
single thread or externally serialized context.

In `@cpp/include/cuopt/linear_programming/utilities/solver_cache_profiler.hpp`:
- Around line 54-116: Guard the singleton profiler state in profiler_t: reset(),
add(), and log_summary() all read/write the shared times_ array without
synchronization, so concurrent OMP task execution can race and corrupt cache
profiling. Add a mutex or other thread-safe protection around the mutable state
in profiler_t::instance(), profiler_t::reset(), profiler_t::add(), and any
readout in profiler_t::log_summary(), or explicitly restrict CUOPT_CACHE_PROFILE
to single-threaded use if locking is not desired.

In `@cpp/src/barrier/barrier.cu`:
- Around line 943-945: The transpose call in the warm-reuse path is using the
operands in the wrong order: in the barrier setup around the AD/AT rebuild,
`AT.transpose(AD)` overwrites the newly prepared `AD` instead of recomputing
`AT` from it. Update the logic so the updated `AD` is the source and `AT` is the
destination, using the `transpose` method on the `AT`/`AD` pair in the barrier
code path, so the subsequent `ad_mat().copy(...)` sees the refreshed values.

In `@cpp/src/barrier/cusparse_view.cu`:
- Around line 257-265: update_matrix_values in cusparse_view_t blindly copies
A.x and A_csr.x into A_T_data_ and A_data_ without checking nnz sizes match the
destination buffers. Add a size/assertion check at the start of
cusparse_view_t<i_t, f_t>::update_matrix_values to verify A.x.size() and
A_csr.x.size() are the same size as A_T_data_ and A_data_ before calling
raft::copy, so the warm-cache reuse path in barrier.cu cannot silently write out
of bounds if the sparsity invariant is broken.

In `@cpp/src/barrier/sparse_cholesky.cuh`:
- Around line 533-534: The cuDSS reuse state is inconsistent because
numeric_factor_valid_ is only being cleared and never actually drives any logic.
Update the sparse_cholesky.cuh cuDSS flow around symbolic_done_ and the
factorization/rebind path to either set and check numeric_factor_valid_ as part
of the reuse contract, or remove the member and its invalidation writes entirely
if it is unused. Make sure the relevant factorization/rebind methods in
SparseCholesky keep the state transitions consistent with the other validity
flags.

In `@cpp/src/pdlp/solve.cu`:
- Around line 1887-1890: The fingerprint computed in the C03 scope is currently
unused, so update the code around compute_problem_fingerprint and the C03 cache
profile block to either remove the call entirely or pass the result into the
cache path if it is needed there. If the intent is only to measure fingerprint
overhead, keep the call in the same scope but add a brief comment clarifying
that it exists for profiling only, and avoid leaving a dropped result with
[[maybe_unused]] unless it is truly intentional.

In `@python/cuopt/cuopt/linear_programming/problem.py`:
- Around line 1671-1701: The cached DataModel refresh in
_refresh_data_model_values only updates objective coefficients and constraint
bounds, so bound, variable type, and MIP start edits from setLowerBound,
setUpperBound, setVariableType, and setMIPStart remain stale on a reused
solve(). Update those setters to invalidate or sync the cached model state, or
extend _refresh_data_model_values to also push the remaining mutable fields
before calling solve(). Keep the fix centered on the Problem methods and model
refresh path so cached values always reflect the latest Python-side changes.

In `@python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py`:
- Around line 80-110: The capture_solver_output context manager currently
redirects stdout and stderr to pipes and only reads them after prob.solve()
finishes, which can deadlock on verbose output. Update capture_solver_output to
use file-backed temporary capture instead of os.pipe/os.dup2 pipes, and keep the
existing restore-and-replay behavior when gathering text from the captured
streams. Preserve the same public behavior of yielding the capture object while
fixing the implementation inside capture_solver_output.

In `@script_perf_eval.py`:
- Around line 110-129: The `_capture_solver_output` context manager can block
`prob.solve()` because it only reads from the pipe after the solver finishes,
allowing the `stderr` pipe buffer to fill up. Update `_capture_solver_output` to
avoid backpressure by either redirecting fd 2 to a temporary file or
continuously draining `read_fd` in a background reader while the solve runs,
then preserve the existing behavior of restoring stderr and forwarding captured
text afterward.

---

Nitpick comments:
In `@cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp`:
- Around line 333-336: Document the ownership and lifetime contract for
lp_solve_session in solver_settings.hpp: make it explicit that this raw pointer
is non-owning, is only valid for the duration of the matching call_solve/solve
flow, and that ownership is transferred later into
linear_programming_ret_t::lp_solve_session (the unique_ptr). Also note that
callers should clear or replace it before unrelated solves or any
structural/sparsity change, and add a short note on any thread-safety
expectations if session_enabled and lp_solve_session are reused across calls.

In `@cpp/src/barrier/barrier_factorization_sparsity_hash.hpp`:
- Around line 54-117: The augmented-KKT sparsity layout is duplicated here and
in iteration_data_t::form_augmented(true), so the hash logic can silently drift
from the real matrix construction. Refactor the shared ordering rules (Q
diagonal handling, A row entries, and AT lower block traversal) into a common
helper and have both hash_augmented_kkt_sparsity and form_augmented use it, so
the host hash stays identical to the production layout and
hash_device_csr_sparsity_pattern checks remain trustworthy.

In `@cpp/src/pdlp/utilities/cython_solve.cu`:
- Around line 144-170: The setup in cython_solve.cu is doing unnecessary work
and misreporting timing: `ephemeral_stream`/`ephemeral_handle` are created
before the `memory_backend_t::GPU` and `want_session` checks even though
`solve_handle` is switched to `active_session->handle_ptr()` in the session
path, and the `else` branch around `C01` records time without measuring any
construction. Update the control flow so `raft::handle_t` is only created in the
non-session path, and place the `cache_profile::C01` timing around the actual
ephemeral handle creation (using the existing `ephemeral_stream`,
`ephemeral_handle`, and `lp_solve_session_t::create`/`handle_ptr` logic) so the
sample reflects real setup cost.

In `@python/cuopt/cuopt/linear_programming/problem.py`:
- Around line 1656-1660: The n_rows assignment in problem.py is using a
redundant isinstance(self.rhs, np.ndarray) conditional because both branches
call len(self.rhs); simplify the expression in the same location by removing the
unnecessary if/else and keeping a single len(self.rhs) computation, preserving
the existing behavior in the problem-related method that builds the row count.

In `@script_perf_eval.py`:
- Around line 208-218: The portfolio objective in portfolio_objective is
materializing a dense diagonal matrix via np.diag(info["D_diag"]) just to
compute the diagonal quadratic term. Replace the x_np @ d_matrix @ x_np path
with the equivalent elementwise formulation using info["D_diag"] directly, and
remove the d_matrix allocation so the objective stays memory-efficient.
- Around line 489-548: Remove the stale cross-mode objective helpers from
script_perf_eval.py: verify_objectives_across_modes,
_print_objective_cross_check, and _write_results are no longer used because
--mode all now routes through _compare_mode_objectives and
_write_bench_artifact. Delete these unused functions and any now-dead references
so the module only keeps the active benchmark flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3bdaaade-7af5-444d-ad1e-def826ca93d5

📥 Commits

Reviewing files that changed from the base of the PR and between ade592a and a5eac9f.

📒 Files selected for processing (32)
  • cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp
  • cpp/include/cuopt/linear_programming/utilities/cython_types.hpp
  • cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp
  • cpp/include/cuopt/linear_programming/utilities/solver_cache_profiler.hpp
  • cpp/src/barrier/CMakeLists.txt
  • cpp/src/barrier/barrier.cu
  • cpp/src/barrier/barrier.hpp
  • cpp/src/barrier/barrier_factorization_sparsity_hash.cu
  • cpp/src/barrier/barrier_factorization_sparsity_hash.hpp
  • cpp/src/barrier/barrier_symbolic_cache.hpp
  • cpp/src/barrier/cusparse_view.cu
  • cpp/src/barrier/cusparse_view.hpp
  • cpp/src/barrier/device_sparse_matrix.cuh
  • cpp/src/barrier/sparse_cholesky.cuh
  • cpp/src/dual_simplex/solve.cpp
  • cpp/src/dual_simplex/solve.hpp
  • cpp/src/pdlp/CMakeLists.txt
  • cpp/src/pdlp/solve.cu
  • cpp/src/pdlp/utilities/cython_solve.cu
  • cpp/src/pdlp/utilities/lp_solve_session.cu
  • python/cuopt/cuopt/linear_programming/problem.py
  • python/cuopt/cuopt/linear_programming/solution/solution.py
  • python/cuopt/cuopt/linear_programming/solver/solver.pxd
  • python/cuopt/cuopt/linear_programming/solver/solver.py
  • python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx
  • python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pxd
  • python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.pyx
  • python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py
  • python/cuopt/cuopt/tests/linear_programming/test_lp_solve_session.py
  • script_perf_eval.py
  • script_session_cache_tests.py

Comment on lines +28 to +63
/**
* @brief Lean GPU solve session: owns RAFT handle + stream and optional barrier symbolic cache.
*
* Created on first solve when session_enabled; reused on subsequent solves with the same capsule.
* Per-solve state (optimization_problem_t, presolve, barrier_lp) remains stack-local.
*/
class lp_solve_session_t {
public:
static std::unique_ptr<lp_solve_session_t> create(unsigned stream_flags);

lp_solve_session_t(lp_solve_session_t&&) noexcept;
lp_solve_session_t& operator=(lp_solve_session_t&&) noexcept;
~lp_solve_session_t();

[[nodiscard]] raft::handle_t* handle_ptr();
[[nodiscard]] raft::handle_t const* handle_ptr() const;
[[nodiscard]] rmm::cuda_stream_view stream_view() const;

/**
* @brief Returns cached symbolic state when valid and @p handle matches the stored handle.
*/
[[nodiscard]] linear_programming::dual_simplex::barrier_symbolic_cache_t<int, double>*
symbolic_cache_for_reuse(raft::handle_t const* handle);

void clear_symbolic_cache();

void store_symbolic_cache(
linear_programming::dual_simplex::iteration_data_t<int, double>& data);

private:
lp_solve_session_t(std::unique_ptr<rmm::cuda_stream> stream,
std::unique_ptr<raft::handle_t> handle);

struct impl;
std::unique_ptr<impl> impl_;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp
printf '\n--- file outline ---\n'
ast-grep outline cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp --view expanded || true
printf '\n--- file excerpt ---\n'
cat -n cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp | sed -n '1,220p'
printf '\n--- search for lp_solve_session_t references ---\n'
rg -n "lp_solve_session_t|thread-safety|thread safety|concurrent|serialize access|session_enabled|symbolic_cache_for_reuse|store_symbolic_cache|clear_symbolic_cache" cpp/include cpp/src -g '!**/build/**' || true

Repository: NVIDIA/cuopt

Length of output: 29589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- lp_solve_session implementation ---\n'
cat -n cpp/src/pdlp/utilities/lp_solve_session.cu | sed -n '1,220p'

printf '\n--- cython solve usage around session lifecycle ---\n'
cat -n cpp/src/pdlp/utilities/cython_solve.cu | sed -n '120,260p'

printf '\n--- public header with session field docs ---\n'
cat -n cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp | sed -n '320,350p'

Repository: NVIDIA/cuopt

Length of output: 13671


Add method docs and spell out the threading contract. The public methods here (create, handle_ptr, stream_view, clear_symbolic_cache, store_symbolic_cache) still need Doxygen comments, and the session should explicitly say whether callers can reuse one instance from multiple threads. The cache mutators are unsynchronized, so the intended access pattern should be stated plainly.

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

In `@cpp/include/cuopt/linear_programming/utilities/lp_solve_session.hpp` around
lines 28 - 63, The public API on lp_solve_session_t is missing Doxygen and an
explicit threading contract. Add comments for create, handle_ptr, stream_view,
clear_symbolic_cache, and store_symbolic_cache describing ownership, return
values, and cache behavior. Also state clearly that lp_solve_session_t is not
thread-safe for concurrent use, and that the unsynchronized cache mutators and
reuse helpers must only be called from a single thread or externally serialized
context.

Source: Path instructions

Comment on lines +54 to +116
class profiler_t {
public:
static profiler_t& instance()
{
static profiler_t prof;
return prof;
}

bool enabled() const { return enabled_; }

void reset()
{
times_.fill(0.0);
}

void add(cache_id id, double seconds)
{
if (!enabled_) { return; }
times_[static_cast<int>(id)] += seconds;
}

double get(cache_id id) const { return times_[static_cast<int>(id)]; }

double total_measured() const
{
double sum = 0.0;
for (double t : times_) {
sum += t;
}
return sum;
}

void log_summary() const
{
if (!enabled_) { return; }
auto emit = [](const char* fmt, ...) {
va_list args;
va_start(args, fmt);
char buf[512];
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
CUOPT_LOG_INFO("%s", buf);
fprintf(stderr, "%s\n", buf);
};
emit("=== Solver cache profile (ms) ===");
for (int i = 0; i < num_cache_ids; ++i) {
const double ms = times_[i] * 1000.0;
emit("Cache profile: %s %.3f", cache_id_label(static_cast<cache_id>(i)), ms);
}
emit("Cache profile: TOTAL measured %.3f", total_measured() * 1000.0);
emit("=== End solver cache profile ===");
}

private:
profiler_t()
{
const char* env = std::getenv("CUOPT_CACHE_PROFILE");
enabled_ = env != nullptr && env[0] != '\0' && std::strcmp(env, "0") != 0;
}

bool enabled_{false};
std::array<double, num_cache_ids> times_{};
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the profiler and its call sites
git ls-files | rg 'solver_cache_profiler|cython_solve|solver_settings|solve\.cu|Concurrent'

# Show the profiler header with line numbers
sed -n '1,220p' cpp/include/cuopt/linear_programming/utilities/solver_cache_profiler.hpp

# Find all references to cache_profile::reset() and profiler_t::instance()
rg -n "cache_profile::reset\(|profiler_t::instance\(|CUOPT_CACHE_PROFILE|method_t::Concurrent|Concurrent" cpp include python -S

# Inspect the likely solve entry point and concurrent execution references
sed -n '1,260p' cpp/src/linear_programming/cython_solve.cu
sed -n '1,260p' cpp/include/cuopt/linear_programming/solver_settings.hpp
sed -n '1,260p' cpp/src/linear_programming/pdlp/solve.cu

Repository: NVIDIA/cuopt

Length of output: 16099


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the solve entry point and profiler reset site
sed -n '90,150p' cpp/src/pdlp/utilities/cython_solve.cu

# Inspect the method_t documentation and default concurrent behavior
sed -n '50,90p' cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp
sed -n '300,330p' cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp

# Inspect the main PDLP solve path around Concurrent handling
sed -n '1810,1865p' cpp/src/pdlp/solve.cu

# Check for any synchronization around the profiler or cache_profile namespace
rg -n "mutex|lock_guard|atomic|cache_profile::|CUOPT_CACHE_PROFILE_SCOPE" cpp/src/pdlp cpp/src/barrier cpp/src/dual_simplex cpp/include/cuopt/linear_programming -S

Repository: NVIDIA/cuopt

Length of output: 12850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the concurrent solve implementation
sed -n '1,220p' cpp/src/pdlp/solve.cu | sed -n '1,220p'
rg -n "run_concurrent|std::thread|std::async|omp|launch|concurrent_halt|Barrier|DualSimplex" cpp/src/pdlp/solve.cu cpp/src/barrier cpp/src/dual_simplex -S

# Inspect the cache-profile usage sites in the concurrent backends
sed -n '1,120p' cpp/src/barrier/barrier.cu
sed -n '1820,1975p' cpp/src/pdlp/solve.cu

Repository: NVIDIA/cuopt

Length of output: 50369


Guard the process-wide profiler state. profiler_t::reset() and add() mutate a singleton times_ array with no synchronization. Since run_concurrent() schedules work on OMP tasks, overlapping timers and concurrent solves can race with reset(), corrupting the reported cache profile. A mutex here would be small; otherwise document that CUOPT_CACHE_PROFILE is single-threaded only.

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

In `@cpp/include/cuopt/linear_programming/utilities/solver_cache_profiler.hpp`
around lines 54 - 116, Guard the singleton profiler state in profiler_t:
reset(), add(), and log_summary() all read/write the shared times_ array without
synchronization, so concurrent OMP task execution can race and corrupt cache
profiling. Add a mutex or other thread-safe protection around the mutable state
in profiler_t::instance(), profiler_t::reset(), profiler_t::add(), and any
readout in profiler_t::log_summary(), or explicitly restrict CUOPT_CACHE_PROFILE
to single-threaded use if locking is not desired.

Source: Path instructions

Comment on lines +943 to +945
AD = A;
if (!use_augmented && n_dense_columns > 0) { AD.remove_columns(cols_to_remove); }
AT.transpose(AD);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm transpose semantics: X.transpose(Y) => Y = X^T
rg -nP --type=cpp -C3 '\btranspose\s*\(' cpp/src/dual_simplex/sparse_matrix.hpp
rg -nP -C1 '\.transpose\(' cpp/src/barrier/barrier.cu

Repository: NVIDIA/cuopt

Length of output: 1453


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sparse_matrix transpose declaration/definition =="
rg -n -C 6 '\btranspose\s*\(' cpp/src/dual_simplex/sparse_matrix.hpp cpp/src/dual_simplex/sparse_matrix.cuh cpp/src/dual_simplex/sparse_matrix.cu

echo
echo "== barrier.cu constructor around transpose =="
sed -n '575,605p' cpp/src/barrier/barrier.cu | cat -n

echo
echo "== barrier.cu refresh_lp_numerics around lines 930-990 =="
sed -n '930,990p' cpp/src/barrier/barrier.cu | cat -n

Repository: NVIDIA/cuopt

Length of output: 1408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== transpose implementation in sparse_matrix.hpp =="
sed -n '1,170p' cpp/src/dual_simplex/sparse_matrix.hpp | cat -n

echo
echo "== barrier.cu constructor and refresh block =="
sed -n '585,605p' cpp/src/barrier/barrier.cu | cat -n
echo
sed -n '935,990p' cpp/src/barrier/barrier.cu | cat -n

Repository: NVIDIA/cuopt

Length of output: 9787


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== class members / initialization paths mentioning AD and AT =="
rg -n -C 4 '\b(AD|AT)\b' cpp/src/barrier/barrier.cu

echo
echo "== refresh_lp_numerics full function =="
ast-grep outline cpp/src/barrier/barrier.cu --match refresh_lp_numerics --view expanded

Repository: NVIDIA/cuopt

Length of output: 14726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== class members / initialization paths mentioning AD and AT =="
rg -n -C 4 '\b(AD|AT)\b' cpp/src/barrier/barrier.cu

echo
echo "== refresh_lp_numerics and nearby helpers =="
rg -n -C 40 'bool refresh_lp_numerics|refresh_lp_numerics\(' cpp/src/barrier/barrier.cu

Repository: NVIDIA/cuopt

Length of output: 21557


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the transpose implementation to confirm argument semantics.
rg -n -C 20 'transpose\(csc_matrix_t<i_t, f_t>& AT\) const' cpp/src/dual_simplex/sparse_matrix.hpp

# Show where AT is consumed after refresh_lp_numerics.
rg -n -C 3 '\bAT\b' cpp/src/barrier/barrier.cu | sed -n '1,120p'

Repository: NVIDIA/cuopt

Length of output: 5294


Swap these transpose operands cpp/src/barrier/barrier.cu:943-945

transpose(out) writes out = this^T, so AT.transpose(AD) overwrites the fresh AD with AT^T instead of rebuilding AT from the updated AD. That leaves ad_mat().copy(AD, ...) uploading stale values into ADAT on the warm-reuse path.

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

In `@cpp/src/barrier/barrier.cu` around lines 943 - 945, The transpose call in the
warm-reuse path is using the operands in the wrong order: in the barrier setup
around the AD/AT rebuild, `AT.transpose(AD)` overwrites the newly prepared `AD`
instead of recomputing `AT` from it. Update the logic so the updated `AD` is the
source and `AT` is the destination, using the `transpose` method on the
`AT`/`AD` pair in the barrier code path, so the subsequent `ad_mat().copy(...)`
sees the refreshed values.

Comment on lines +257 to +265
template <typename i_t, typename f_t>
void cusparse_view_t<i_t, f_t>::update_matrix_values(const csc_matrix_t<i_t, f_t>& A)
{
const auto stream = handle_ptr_->get_stream();
raft::copy(A_T_data_.data(), A.x.data(), A.x.size(), stream);
csr_matrix_t<i_t, f_t> A_csr(A.m, A.n, 1);
A.to_compressed_row(A_csr);
raft::copy(A_data_.data(), A_csr.x.data(), A_csr.x.size(), stream);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add size validation before overwriting A_T_data_/A_data_.

update_matrix_values copies A.x.size() / A_csr.x.size() elements into A_T_data_ / A_data_ without checking those destination buffers were sized for the same nnz. This method is designed to be called only during warm symbolic-cache reuse where sparsity (and hence nnz) is guaranteed unchanged by the caller in barrier.cu, but that invariant isn't enforced here — a future caller or a bug in the hash-matching gate upstream would cause a silent out-of-bounds device write.

🔒 Proposed fix: assert size match before copy
 template <typename i_t, typename f_t>
 void cusparse_view_t<i_t, f_t>::update_matrix_values(const csc_matrix_t<i_t, f_t>& A)
 {
   const auto stream = handle_ptr_->get_stream();
+  cuopt_assert(A.x.size() == A_T_data_.size(),
+               "update_matrix_values: nnz mismatch for A_T_data_ (sparsity changed?)");
   raft::copy(A_T_data_.data(), A.x.data(), A.x.size(), stream);
   csr_matrix_t<i_t, f_t> A_csr(A.m, A.n, 1);
   A.to_compressed_row(A_csr);
+  cuopt_assert(A_csr.x.size() == A_data_.size(),
+               "update_matrix_values: nnz mismatch for A_data_ (sparsity changed?)");
   raft::copy(A_data_.data(), A_csr.x.data(), A_csr.x.size(), stream);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template <typename i_t, typename f_t>
void cusparse_view_t<i_t, f_t>::update_matrix_values(const csc_matrix_t<i_t, f_t>& A)
{
const auto stream = handle_ptr_->get_stream();
raft::copy(A_T_data_.data(), A.x.data(), A.x.size(), stream);
csr_matrix_t<i_t, f_t> A_csr(A.m, A.n, 1);
A.to_compressed_row(A_csr);
raft::copy(A_data_.data(), A_csr.x.data(), A_csr.x.size(), stream);
}
template <typename i_t, typename f_t>
void cusparse_view_t<i_t, f_t>::update_matrix_values(const csc_matrix_t<i_t, f_t>& A)
{
const auto stream = handle_ptr_->get_stream();
cuopt_assert(A.x.size() == A_T_data_.size(),
"update_matrix_values: nnz mismatch for A_T_data_ (sparsity changed?)");
raft::copy(A_T_data_.data(), A.x.data(), A.x.size(), stream);
csr_matrix_t<i_t, f_t> A_csr(A.m, A.n, 1);
A.to_compressed_row(A_csr);
cuopt_assert(A_csr.x.size() == A_data_.size(),
"update_matrix_values: nnz mismatch for A_data_ (sparsity changed?)");
raft::copy(A_data_.data(), A_csr.x.data(), A_csr.x.size(), stream);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/barrier/cusparse_view.cu` around lines 257 - 265,
update_matrix_values in cusparse_view_t blindly copies A.x and A_csr.x into
A_T_data_ and A_data_ without checking nnz sizes match the destination buffers.
Add a size/assertion check at the start of cusparse_view_t<i_t,
f_t>::update_matrix_values to verify A.x.size() and A_csr.x.size() are the same
size as A_T_data_ and A_data_ before calling raft::copy, so the warm-cache reuse
path in barrier.cu cannot silently write out of bounds if the sparsity invariant
is broken.

Comment on lines +533 to +534
symbolic_done_ = true;
numeric_factor_valid_ = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and symbol usages.
git ls-files cpp/src/barrier/sparse_cholesky.cuh
printf '\n--- symbol occurrences ---\n'
rg -n "numeric_factor_valid_|symbolic_done_|factorize|solve|invalidate_numeric_factor|rebind_csr_matrix|rebind_settings" cpp/src/barrier/sparse_cholesky.cuh

printf '\n--- targeted ranges around occurrences ---\n'
sed -n '120,170p' cpp/src/barrier/sparse_cholesky.cuh
sed -n '500,560p' cpp/src/barrier/sparse_cholesky.cuh
sed -n '860,970p' cpp/src/barrier/sparse_cholesky.cuh

Repository: NVIDIA/cuopt

Length of output: 12870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show file size and the whole symbol context in smaller slices if needed.
wc -l cpp/src/barrier/sparse_cholesky.cuh

Repository: NVIDIA/cuopt

Length of output: 190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search the repository for all references to numeric_factor_valid_.
rg -n "numeric_factor_valid_" .

Repository: NVIDIA/cuopt

Length of output: 575


numeric_factor_valid_ is dead state in the cuDSS path

numeric_factor_valid_ is only ever cleared here and in the invalidation paths, but never read or set true, so it does not participate in the reuse contract. Either wire it into the post-factorization/rebind flow or remove it to avoid misleading state.

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

In `@cpp/src/barrier/sparse_cholesky.cuh` around lines 533 - 534, The cuDSS reuse
state is inconsistent because numeric_factor_valid_ is only being cleared and
never actually drives any logic. Update the sparse_cholesky.cuh cuDSS flow
around symbolic_done_ and the factorization/rebind path to either set and check
numeric_factor_valid_ as part of the reuse contract, or remove the member and
its invalidation writes entirely if it is unused. Make sure the relevant
factorization/rebind methods in SparseCholesky keep the state transitions
consistent with the other validity flags.

Source: Path instructions

Comment thread cpp/src/pdlp/solve.cu
Comment on lines +1887 to +1890
{
CUOPT_CACHE_PROFILE_SCOPE(cuopt::linear_programming::cache_profile::cache_id::C03);
[[maybe_unused]] const uint64_t fingerprint = compute_problem_fingerprint(op_problem);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant symbols first.
ast-grep outline cpp/src/pdlp/solve.cu --view expanded

# Read the surrounding section with line numbers.
sed -n '60,110p;1860,1915p' cpp/src/pdlp/solve.cu

# Search for all uses of the fingerprint helper and its return value.
rg -n "compute_problem_fingerprint|fnv1a64_mix|fingerprint" cpp/src/pdlp/solve.cu cpp/src/pdlp -S

Repository: NVIDIA/cuopt

Length of output: 6866


Fingerprint result is unused inside the C03 scope — the value is computed from host copies and then dropped. Remove the call or wire it into the cache path; if it is only there to profile fingerprint cost, add a brief comment.

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

In `@cpp/src/pdlp/solve.cu` around lines 1887 - 1890, The fingerprint computed in
the C03 scope is currently unused, so update the code around
compute_problem_fingerprint and the C03 cache profile block to either remove the
call entirely or pass the result into the cache path if it is needed there. If
the intent is only to measure fingerprint overhead, keep the call in the same
scope but add a brief comment clarifying that it exists for profiling only, and
avoid leaving a dropped result with [[maybe_unused]] unless it is truly
intentional.

Comment on lines +1671 to +1701
def _refresh_data_model_values(self):
"""Patch existing DataModel when sparsity structure is unchanged."""
n = len(self.vars)
if (
self.model is None
or self.constraint_csr_matrix is None
or self.objective is None
or len(self.objective) != n
or self.rhs is None
):
self._to_data_model()
return

for j in range(n):
self.objective[j] = self.vars[j].getObjectiveCoefficient()

rhs_arr = (
self.rhs
if isinstance(self.rhs, np.ndarray)
else np.asarray(self.rhs, dtype=np.float64)
)
linear_row = 0
for constr in self.constrs:
if constr.is_quadratic:
continue
rhs_arr[linear_row] = constr.RHS
linear_row += 1
self.rhs = rhs_arr

self.model.set_objective_coefficients(self.objective)
self.model.set_constraint_bounds(rhs_arr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="python/cuopt/cuopt/linear_programming/problem.py"

echo "== File size =="
wc -l "$FILE"

echo "== Outline =="
ast-grep outline "$FILE" --view expanded | sed -n '1,260p'

echo "== Relevant symbols search =="
rg -n "def (_refresh_data_model_values|update|perturb|set_.*objective|set_.*bound|ObjConstant|objective_qmatrix|constraint_csr_matrix|rhs|solve|addVariable|addConstraint)" "$FILE"

Repository: NVIDIA/cuopt

Length of output: 5803


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="python/cuopt/cuopt/linear_programming/problem.py"

sed -n '73,220p' "$FILE"
echo "-----"
sed -n '1284,1408p' "$FILE"
echo "-----"
sed -n '1452,1745p' "$FILE"
echo "-----"
sed -n '1792,1965p' "$FILE"
echo "-----"
sed -n '2315,2355p' "$FILE"

Repository: NVIDIA/cuopt

Length of output: 27373


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="python/cuopt/cuopt/linear_programming/problem.py"

sed -n '1945,1998p' "$FILE"
echo "-----"
sed -n '145,180p' "$FILE"
echo "-----"
sed -n '155,175p' "$FILE"
echo "-----"
sed -n '1831,1878p' "$FILE"

Repository: NVIDIA/cuopt

Length of output: 5286


_refresh_data_model_values leaves bound/type edits stale.
solve() reuses the cached DataModel, but setLowerBound(), setUpperBound(), setVariableType(), and setMIPStart() only change Python-side state. Re-solving after one of those edits keeps the old model unless callers remember to call update() first. Either invalidate the cache in those setters or refresh the remaining mutable fields here.

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

In `@python/cuopt/cuopt/linear_programming/problem.py` around lines 1671 - 1701,
The cached DataModel refresh in _refresh_data_model_values only updates
objective coefficients and constraint bounds, so bound, variable type, and MIP
start edits from setLowerBound, setUpperBound, setVariableType, and setMIPStart
remain stale on a reused solve(). Update those setters to invalidate or sync the
cached model state, or extend _refresh_data_model_values to also push the
remaining mutable fields before calling solve(). Keep the fix centered on the
Problem methods and model refresh path so cached values always reflect the
latest Python-side changes.

Comment on lines +80 to +110
@contextmanager
def capture_solver_output():
"""Capture solver stdout and stderr (C++ logs use both)."""
read_out, write_out = os.pipe()
read_err, write_err = os.pipe()
saved_out = os.dup(1)
saved_err = os.dup(2)
capture = io.StringIO()
try:
os.dup2(write_out, 1)
os.dup2(write_err, 2)
os.close(write_out)
os.close(write_err)
yield capture
finally:
os.dup2(saved_out, 1)
os.dup2(saved_err, 2)
os.close(saved_out)
os.close(saved_err)
with os.fdopen(read_out, "r", encoding="utf-8", errors="replace") as reader_out:
out_text = reader_out.read()
with os.fdopen(read_err, "r", encoding="utf-8", errors="replace") as reader_err:
err_text = reader_err.read()
text = out_text + err_text
capture.write(text)
capture.seek(0)
if text:
sys.stdout.write(out_text)
sys.stdout.flush()
sys.stderr.write(err_text)
sys.stderr.flush()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file map ---\n'
git ls-files python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py
printf '\n--- relevant lines ---\n'
nl -ba python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py | sed -n '1,220p'

printf '\n--- usages ---\n'
rg -n "capture_solver_output|session_cache_helpers" python/cuopt/cuopt/tests -S

Repository: NVIDIA/cuopt

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file map ---'
git ls-files python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py

echo
echo '--- relevant lines ---'
nl -ba python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py | sed -n '1,220p'

echo
echo '--- usages ---'
rg -n "capture_solver_output|session_cache_helpers" python/cuopt/cuopt/tests -S

Repository: NVIDIA/cuopt

Length of output: 300


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file ---'
cat -n python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py | sed -n '1,220p'

echo
echo '--- usages ---'
rg -n "capture_solver_output|session_cache_helpers" python/cuopt/cuopt/tests -S

echo
echo '--- solver log mentions in linear_programming tests ---'
rg -n "stdout|stderr|cache-profile|iteration|barrier|log" python/cuopt/cuopt/tests/linear_programming -S

Repository: NVIDIA/cuopt

Length of output: 27828


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- solve_with_log and assertions ---'
sed -n '260,360p' python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py

echo
echo '--- session tests ---'
sed -n '1,260p' python/cuopt/cuopt/tests/linear_programming/test_lp_solve_session.py

Repository: NVIDIA/cuopt

Length of output: 12469


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- log string definitions in C++ ---'
rg -n "Barrier: reusing cuDSS symbolic analysis|Barrier: rebuilt cuDSS symbolic analysis|Barrier: stored augmented symbolic cache hash|Barrier: stored ADAT symbolic cache hash|Barrier: hash match but numeric refresh failed|=== Solver cache profile ===|Cache profile: C" cpp python -S

echo
echo '--- verbosity / logging settings near barrier solver ---'
rg -n "iteration|log|printf|stderr|stdout|cache profile|CUOPT_CACHE_PROFILE" cpp/src python/cuopt/cuopt/tests -S | sed -n '1,220p'

Repository: NVIDIA/cuopt

Length of output: 25031


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- barrier logging around reuse path ---'
sed -n '4460,4565p' cpp/src/barrier/barrier.cu

echo
echo '--- barrier iteration logging sites ---'
rg -n "settings\.log\.printf|printf\(" cpp/src/barrier/barrier.cu -n -S | sed -n '1,200p'

Repository: NVIDIA/cuopt

Length of output: 15328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- barrier iteration loop near log lines ---'
sed -n '4628,4868p' cpp/src/barrier/barrier.cu

echo
echo '--- logger default / iteration log frequency ---'
rg -n "iteration_log_frequency|first_iteration_log|debug|log\.printf\(" cpp/src/barrier cpp/src/dual_simplex -S | sed -n '1,220p'

Repository: NVIDIA/cuopt

Length of output: 35675


Capture solver logs into files instead of pipes
capture_solver_output() drains stdout/stderr only after prob.solve() returns, so a verbose solve can fill the pipe buffer and block the writer indefinitely. A TemporaryFile-based capture avoids that hang.

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

In `@python/cuopt/cuopt/tests/linear_programming/session_cache_helpers.py` around
lines 80 - 110, The capture_solver_output context manager currently redirects
stdout and stderr to pipes and only reads them after prob.solve() finishes,
which can deadlock on verbose output. Update capture_solver_output to use
file-backed temporary capture instead of os.pipe/os.dup2 pipes, and keep the
existing restore-and-replay behavior when gathering text from the captured
streams. Preserve the same public behavior of yielding the capture object while
fixing the implementation inside capture_solver_output.

Comment thread script_perf_eval.py
Comment on lines +110 to +129
@contextmanager
def _capture_solver_output():
"""Capture C++ solver logs written directly to stderr (fd 2)."""
read_fd, write_fd = os.pipe()
saved_stderr = os.dup(2)
capture = io.StringIO()
try:
os.dup2(write_fd, 2)
os.close(write_fd)
yield capture
finally:
os.dup2(saved_stderr, 2)
os.close(saved_stderr)
with os.fdopen(read_fd, "r", encoding="utf-8", errors="replace") as reader:
text = reader.read()
capture.write(text)
capture.seek(0)
if text:
sys.stderr.write(text)
sys.stderr.flush()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and inspect the relevant region with line numbers.
git ls-files | rg '^script_perf_eval\.py$|(^|/)script_perf_eval\.py$'
wc -l script_perf_eval.py
sed -n '1,260p' script_perf_eval.py | cat -n

# Find where _capture_solver_output is used and whether the captured output is read incrementally.
rg -n "_capture_solver_output|prob\.solve|stderr|CUOPT_CACHE_PROFILE|barrier" script_perf_eval.py

Repository: NVIDIA/cuopt

Length of output: 11625


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the other stderr-capture call site and nearby logic.
sed -n '450,520p' script_perf_eval.py | cat -n

# Inspect the section that mentions stderr buffering / cache profile output.
sed -n '730,770p' script_perf_eval.py | cat -n

# Look for any background draining or threaded stderr handling in this script.
rg -n "thread|Thread|drain|read_fd|TemporaryFile|Popen|stderr" script_perf_eval.py

Repository: NVIDIA/cuopt

Length of output: 5325


Pipe-based stderr capture can stall prob.solve(). _capture_solver_output() only drains the pipe after the solve returns, so solver writes during the call can fill the bounded pipe buffer and block the process. Redirect to a temp file or drain it in a background reader instead.

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

In `@script_perf_eval.py` around lines 110 - 129, The `_capture_solver_output`
context manager can block `prob.solve()` because it only reads from the pipe
after the solver finishes, allowing the `stderr` pipe buffer to fill up. Update
`_capture_solver_output` to avoid backpressure by either redirecting fd 2 to a
temporary file or continuously draining `read_fd` in a background reader while
the solve runs, then preserve the existing behavior of restoring stderr and
forwarding captured text afterward.

self._to_data_model()
return

for j in range(n):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Vectorized update will be faster than looping over n.

return None
if self._constraint_csr_scipy is not None:
return self._constraint_csr_scipy
n_rows = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant branching here. Can set n_rows = len(self.rhs)

for j in range(n):
self.objective[j] = self.vars[j].getObjectiveCoefficient()

rhs_arr = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant branching here.

)
return self._constraint_csr_scipy

def _refresh_data_model_values(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only linear cost and rhs of linear constraints are updated. Missing update of quadratic cost, lower and upper bounds of variables, constraint matrix, row sense and objective constraint.

if A is None:
return False

rhs_arr = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant branching here.

self.model.set_objective_coefficients(self.objective)
self.model.set_constraint_bounds(rhs_arr)

def _populate_slacks_vectorized(self, primal_sol):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need to recompute slack values rather than extract them from existing values in the solver?

constr.DualValue = dual_sol[linear_row]
constr.Slack = constr.compute_slack()
linear_row += 1
if not (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What are these new lines 2291-2312 for?`

@@ -2212,10 +2337,13 @@ def solve(self, settings=solver_settings.SolverSettings()):
>>> problem.setObjective(x + y, sense=MAXIMIZE)
>>> problem.solve()
"""
if self.model is None:
if self.model is None or self.constraint_csr_matrix is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why we need additional self.constraint_csr_matrix is None here?

self._refresh_data_model_values()
active_session = session if session is not None else self._session
solution = solver.Solve(self.model, settings, session=active_session)
if getattr(settings, "session_enabled", False) and solution.lp_solve_session is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If session_enabled=False. Why do we still set self._session = solution.lp_solve_session?

*/
/* clang-format on */

#include <cuopt/linear_programming/utilities/lp_solve_session.hpp>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's better to move lp_solve_session files under barrier folder. barrier_resolve_session may be a better name.

#include <optional>
#include <utility>

namespace cuopt::cython {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

solver_session is not limited to the use in python. Move it out of namespace cuopt::cython.

auto& pdlp_settings = solver_settings->get_pdlp_settings();
const bool session_enabled = pdlp_settings.session_enabled;
const bool barrier_path = uses_barrier_session_path(*solver_settings, *data_model);
const bool want_session = (session_in != nullptr || session_enabled) && barrier_path &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we also have to exclude concurrent mode?

const raft::handle_t handle_{stream};
if (memory_backend == cuopt::linear_programming::memory_backend_t::GPU) {
if (want_session) {
if (active_session == nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Moving timing one layer up instead of repetition in different branch.

Comment on lines +141 to +144
lp_solve_session_t* active_session = session_in;
pdlp_settings.lp_solve_session = nullptr;

rmm::cuda_stream ephemeral_stream(static_cast<rmm::cuda_stream::flags>(flags));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we create a new stream rather than using the existing one in lp_solve_session_t?

Comment on lines +334 to +336
bool session_enabled{false};
/** Non-owning session pointer set by ``call_solve`` for barrier symbolic reuse. */
cuopt::cython::lp_solve_session_t* lp_solve_session{nullptr};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we separate the session pointer from solver_settings_t struct? It is set to nullptr in every call of call_solve in cython_solve.cu. Parameters in solver_settings_t shouldn't be changed within the algorithm call.

Comment thread cpp/src/pdlp/solve.cu
namespace cuopt::mathematical_optimization {
namespace cuopt::linear_programming {

namespace {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What are these lines of code for?

Comment thread cpp/src/pdlp/solve.cu
@@ -1821,10 +1884,14 @@ optimization_problem_solution_t<i_t, f_t> solve_qcqp(
CUOPT_LOG_INFO("Writing user problem to file: %s", settings.user_problem_file.c_str());
op_problem.write_to_mps(settings.user_problem_file);
}
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is this step for?

{
}

void clear()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We also need to clear vector and matrix memory at the same time.

raft::handle_t const* handle) const
{
return valid && handle != nullptr && handle_ptr == handle && use_augmented == augmented &&
sparsity_hash == hash;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The hash comparison is a simplified but not theoretically equivalent one. Can we have an exact comparison of row, col vectors in csr format, e.g. the same size of vectors and the same index and shift values in them? This should also be efficient on GPU.

handle_ptr_->get_stream().synchronize();

symbolic_done_ = true;
numeric_factor_valid_ = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems to be never used.

settings_ = &settings;
}

void invalidate_numeric_factor() override { numeric_factor_valid_ = false; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The function can be removed since numeric_factor_valid_ is not used.

this->positive_definite = positive_definite;
}

void rebind_settings(const simplex_solver_settings_t<i_t, f_t>& settings) override

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want additional parameter settings update beyond value update in objective and constraints?

csc_matrix_t<i_t, f_t> Q(lp.num_cols, 0, 0);
std::unique_ptr<iteration_data_t<i_t, f_t>> owned_data;

auto finish_session = [&](lp_status_t status) -> lp_status_t {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Define finish_session explicitly outside of barrier_solver_t<i_t, f_t>::solve

// Build the sparsity pattern of the augmented system
form_augmented(true);

auto adopt_augmented_symbolic = [&]() -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Define a function explicitly outside.

rmm::device_uvector<i_t> d_augmented_diagonal_indices_;
rmm::device_uvector<i_t> d_cone_csr_indices_;
rmm::device_uvector<f_t> d_cone_Q_values_;
device_csr_matrix_t<i_t, f_t>* pinned_device_augmented_{nullptr};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's a bit strange we have pinned memory defined here. Why we need this additional pointer if we already have device_ vectors in the existing iteration_data_t.

If it is related to the issue that reusable space outlive iteration_data_t. We'd better group all reusable memory into a struct and shared it to iteration_data_t.


template <typename i_t, typename f_t>
lp_status_t barrier_solver_t<i_t, f_t>::solve(f_t start_time, lp_solution_t<i_t, f_t>& solution)
lp_status_t barrier_solver_t<i_t, f_t>::solve(f_t start_time,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some thoughts on the new solve function:

Current solve function of barrier_solver_t can be divided into two parts: the first phase is the initialization of all data structures and vectors we needed, and the second phase is the true solve phase (running a barrier method). We can split it into a setup for the first phase and a solve function for the second phase. Then, all changes relevant to reoptimization reside in setup function only.

Additionally, we can verify if reusing previous factorization is possible or not once the barrier_presolve is finished, by verifying objective and constraints (vectors and matrices) are the same as the previous problem. That means we can possibly reuse iteration_data_t by updating only the reusing workspace.

static_cast<unsigned long long>(cache.sparsity_hash));
}

bool refresh_augmented_values()

@yuwenchen95 yuwenchen95 Jul 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's better to define refresh functions for each blocks (e.g. Q, A, ) respectively. Only Q and A entries in the augmented KKT needs to be refreshed in the setup of reoptimization, since other entries will be recomputed for every barrier iteration.

In addition, updating blocks seperately also help in speed, since users usually update a subset of problem coefficients and we may not need to update all numerical values.

* that was analyzed, and path-specific GPU workspace (augmented KKT or ADAT + cuSPARSE).
*/
template <typename i_t, typename f_t>
struct barrier_symbolic_cache_t {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need to save previous problem coefficients, e.g. Q, A and bounds, etc, for checking if reoptimization is possible.

return sol, log_text, profile


def test_adat_session_warm_reuse():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These tests seems to be generated by agent. It's best we clearly define a deterministic small examples rather than relying on random generation in unit tests.

Also, it would be clear to define a problem in lp format for better visualization.

expect_augmented: bool = False,
) -> None:
"""Cold run stores cache; warm run reuses symbolic factorization."""
if expect_adat:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Capturing lines in log is fragile since we may not be able to include so much info in logging and the logging will vary over time. The best way is to clearly define a data_update function where we update new problem coefficients and return the flag that if previous factorization could be reused or not.

@@ -0,0 +1,150 @@
#!/usr/bin/env python3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's a test file that have to be under test folder.

Comment thread script_perf_eval.py
f"(cold={bench.get('cold_reuse_log_count')}, warm={warm_reuse})"
)
if c07_w < 50.0:
raise AssertionError(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The check based on time heuristic is very fragile, the result may not be consistent across different hardwares and also vary depending on the dimension of a problem. We need clear flag information.

Comment thread script_perf_eval.py
@@ -0,0 +1,927 @@
#!/usr/bin/env python3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's a test file that have to be under test folder.

Also try to rename the file as portfolio_optimization_test.py.


namespace cuopt::linear_programming::cache_profile {

enum class cache_id : int {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it only for temporary code verification?

Comment thread script_perf_eval.py
"C09": "Device buffer allocation sizes",
}

_CACHE_PROFILE_LINE = re.compile(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The same concern as in session_cache_helpers.py: We need clear flag information returned by a check function rather than capturing log or time heuristics.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants