Skip to content

perf(pdlp): widen small-problem termination-check stride (10 → 25) - #1626

Draft
maxwbuckley wants to merge 1 commit into
NVIDIA:mainfrom
maxwbuckley:opt/2-pdlp-termcheck
Draft

perf(pdlp): widen small-problem termination-check stride (10 → 25)#1626
maxwbuckley wants to merge 1 commit into
NVIDIA:mainfrom
maxwbuckley:opt/2-pdlp-termcheck

Conversation

@maxwbuckley

Copy link
Copy Markdown

What

In conditional_major() (cpp/src/pdlp/utils.cuh), widen the base termination/convergence-check stride for the sub-1000-iteration regime from 10 → 25. Larger regimes keep the existing ×10 scaling.

Why

The termination evaluation is an un-graphed chain of ~12–15 eager kernel launches plus a blocking cudaStreamSynchronize — disproportionately expensive next to a single graphed PDHG step on small edge/MPC-sized LPs. At stride 10, a controller-sized LP that converges in a few hundred iterations pays 20–40 of these evaluations. Widening the stride trades a few extra cheap graphed PDHG steps of convergence-detection latency for ~2.5× fewer expensive evaluations — a net win in the small-problem regime that dominates on-robot / edge use.

Correctness

Not bit-identical: convergence is detected up to stride−1 iterations later, so a few extra PDHG steps may run; the reported optimum and termination status are unchanged. Verified — every benchmark objective matches the HiGHS reference (rel < 1e-3) across the whole suite; only the iteration count moves (usually by a handful).

Benchmarks

Measured A/B on an RTX 5090 (WSL2, CUDA 13.2 conda toolkit) with a resident-solver latency harness. Interleaved base/opt invocations (order alternated each round) to avoid sequential-process cache bias; metric is the solver-internal GPU solve_ms; n = 120 per cell (4 rounds × 30 reps). Baseline and this commit differ by exactly this one change.

problem base ms opt ms uplift Welch p
assign_20x100 63.0 44.9 +28.8% 4e-29
assign_10x50 49.0 40.4 +17.4% 1e-18
mpc_H10 69.7 58.2 +16.4% 1e-9
mpc_H80 164.8 138.6 +15.9% 1e-23
assign_5x20 44.2 37.8 +14.4% 3e-20
mpc_H160 149.5 130.2 +12.9% 7e-16
mpc_H40 203.1 181.8 +10.5% 2e-10
mpc_H20 403.2 403.5 −0.1% ns

Median +15.1% across 8 problems (range −0.1% … +28.8%, mean +14.5%), statistically significant on 7 of 8. The mechanism is visible in the data: the optimized path runs slightly more PDHG iterations but far fewer expensive termination evaluations, netting faster with the same optimum.

Why this is a draft / open questions

  • mpc_H20 is neutral — it runs ~700 extra PDHG iterations that exactly offset the evaluation savings. Worth understanding before finalizing.
  • The 25 constant is a first cut. It should be re-tuned against measured net solve time rather than fixed by intuition.
  • Noise floor on the WSL2 test rig is ≈ ±10%/problem (established separately with a bit-identical control change), so the aggregate significance is what establishes the win, not any single row. Re-validation on bare-metal Linux would tighten the numbers.

Feedback on the tuning approach and on whether a size-parameterized stride (vs. a flat 25) is preferred would be welcome.

🤖 Generated with Claude Code

conditional_major() controls how often PDLP runs a full termination/convergence
evaluation in the sub-1000-iteration regime. That evaluation is an un-graphed
~12-15 kernel eager launch chain plus a blocking cudaStreamSynchronize, which is
disproportionately expensive next to a single graphed PDHG step on small
edge/MPC-sized LPs. At the default stride of 10, a controller-sized LP that
converges in a few hundred iterations pays 20-40 of these evaluations.

Widen the small-problem stride to 25, trading a few extra cheap graphed PDHG
steps of convergence-detection latency for roughly 2.5x fewer expensive
evaluations. Larger regimes keep the x10 scaling.

Not bit-identical: convergence is detected up to stride-1 iterations later, so a
few extra PDHG steps may run; the reported optimum and termination status are
unchanged. The constant should be re-tuned against measured net solve time on
the mpc_H*/assign_* suite (bench_lp_robotics).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Max Buckley <maxwbuckley@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 26, 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.

@mlubin
mlubin requested a review from Kh4ster July 27, 2026 14:10
@Kh4ster

Kh4ster commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Hi @maxwbuckley thank you for this contribution. We don't have a lot of those problems were: 1. only need a few amount of iterations (few hundreds) 2. the matrix is small 3. we can't batch (A matrix is the same across many similar LPs).

I have a few questions:

  1. Do you think that "cuda graphing" may solve the problem?
  2. If you are solving multiple LPs on the GPU, wouldn't it be fair to benchmark under LAZY module loading since in practice those kernels would be loaded in memory
  3. We could solve this problem for you by simply exposing this as a pdlp hyper parameter at the C++ layer if you are ok with it

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