Hoist loop-invariant scaling reciprocal out of the LDP inner loops#160
Merged
Merged
Conversation
daqp_update_M and daqp_update_v divided by work->scaling[...] inside the inner loop, once per element, although the divisor is invariant across that loop. Compute the reciprocal once per column and multiply instead. FP division is several times slower than multiplication on most targets, notably the in-order ARM cores used in Raspberry Pis, and these dense O(n^2) loops dominate the LDP transform on the warm-update path. The result differs only by the rounding of x/s vs x*(1/s); the default build already enables -fassociative-math, so bit-exactness is not guaranteed. Full Python test suite (21 tests, incl. the update/warm-start paths that exercise these branches) passes.
cl445
marked this pull request as ready for review
July 10, 2026 10:48
Owner
|
Thank you! This optimization make sense. With this type of optimization, I think that one can break out yet another factor from the inner loop. So instead of for(; j<n; ++j){// Take into account scaling in Rinv
c_float col_scaling = A[disp2-j]/work->scaling[n-j-1];
for(i=0;i<j;++i)
work->M[disp2-i] += work->Rinv[--disp]*col_scaling;
work->M[disp2-j]= work->Rinv[--disp]*col_scaling;
} |
Contributor
Author
|
Yes, that makes sense to me. It’s strictly better than what I had. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
In the QP-to-LDP transform,
daqp_update_Manddaqp_update_vdivide bywork->scaling[...]inside the inner loop, once per element. The divisor is the same across that loop; it depends only on the column, not the inner index. Floating-point division is several times slower than multiplication on most targets, and the gap is widest on the in-order ARM cores used in Raspberry Pis (Cortex-A53/A72), where these dense O(n^2) loops dominate the transform.Change
Compute the reciprocal once per column and multiply:
and the analogous loop in
daqp_update_v. This removes the O(n^2) divisions from the scaling branch and keeps the same multiply order. These branches run on warm updates that reuse theHfactorization (the MPC-style path), not on a fresh solve.Testing
Correctness: full Python test suite passes (21 tests), including
test_model_update_bounds,test_model_update_cost_vector, and the warm-start tests that drive these branches.Speed: standalone C microbenchmark calling
daqp_update_M+daqp_update_vin a loop (no Python or Cython overhead), baseline vs this branch, identical compiler flags, median of 3 runs on Apple Silicon (arm64):The relative gain grows with n: the removed divisions are O(n^2) while the fixed per-call cost is lower order. This is an out-of-order core where FP division is comparatively cheap, so an in-order Cortex-A (Raspberry Pi 3 / Zero 2, where FP64 division is not pipelined) should gain more. The benchmark measures the transform in isolation, not an end-to-end solve.
Downstream CI
PolyDAQP.jlfails on the x86 CI runner, while DAQP's own C, Python, Julia, and MATLAB suites pass, as do the other three downstream packages (ASCertain, LinearMPC, ParametricDAQP). The failing assertion is in the elimination test, which builds a random 6D polytope, projects out three variables, and checks the bounding box matches:I reproduced this locally on arm64, running the elimination test over 2000 random seeds with both the unmodified master build and this branch:
So the failure is a pre-existing over-approximation in
PolyDAQP.eliminate, not a regression from this change: on borderline random polytopes the projection comes out too large by up to ~2.5e-4, which the test's δ = 1e-8 does not tolerate. It is not a solver-tolerance knob either (tighteningprimal_tolin the redundancy step does not reduce it and makes some instances worse), so it looks like the iterated Fourier-Chernikov step itself. The CI runner is x86, where the transform rounds differently than arm64, so the one seeded CI instance happens to tip there; the same class of instance already fails on master for about 0.4% of seeds.I filed the analysis and a reproduction as darnstrom/PolyDAQP.jl#3. This PR does not affect that behavior (bit-identical downstream on arm64), so the red check should clear once the issue is addressed, by improving
eliminateor relaxing the test's δ.