Skip to content

perf: apply the delete step's 2x2 with BLAS rot - #12

Merged
tschm merged 1 commit into
mainfrom
perf_blas_rot_delete_chase
Aug 6, 2026
Merged

perf: apply the delete step's 2x2 with BLAS rot#12
tschm merged 1 commit into
mainfrom
perf_blas_rot_delete_chase

Conversation

@tschm

@tschm tschm commented Aug 6, 2026

Copy link
Copy Markdown
Member

qr_delete's chase applies a 2×2 reflection to a pair of Q's columns once per
step. Spelled out in NumPy that allocates two n-vectors per call:

combined = gc * first + gs * second   # allocates
second *= -gc
second += gs * first                   # allocates
first[:] = combined

BLAS rot does the same work in place, writing through to Q's Fortran-ordered
buffer. Only _mix and a new _ROT handle change — qr_delete,
_reflection_2x2 and _swap are byte-identical to main.

Why the contiguity guard is load-bearing

BLAS has no 2×2 reflection, only the rotation, whose second output is the exact
negation of the reflection's — so one sign flip (exact in IEEE) recovers the
reference's convention.

The guard is not defensive boilerplate. Handed a strided view, f2py copies and
silently drops the overwrite
, returning a correct-looking array while leaving
the original untouched — a wrong answer with no error. Verified directly. It is
the same hazard _reflect already handles for dger at _qr.py:146. In the
solver J is always Fortran-ordered, so the fast path is what production takes
(instrumented: 5516 drot calls, zero fallbacks); the fallback is covered by
test_qr.py, which passes non-contiguous arrays.

Measurements

baseline with drot
n=50, m=80 10.79 ms 10.20 ms 5.5%
n=200, m=300 53.31 ms 49.07 ms 8.0%
n=500, m=700 269.99 ms 251.67 ms 6.8%

2.0–3.0× on the pairwise update itself. Inert on problems that never drop a
constraint
— the first benchmark I ran showed no gain at all, because profiling
later established it triggered zero deletions.

_mix call counts are identical between the two versions (758 / 3976 / 14970),
an independent check that the active-set path did not shift.

Correctness

  • 867 differential tests against the C reference pass — these compare all six
    return values including iteration counts and the active set, so the
    active-set path is unchanged, not merely the minimiser.
  • test_qr.py (66 tests) checks both invariants directly: J Jᵀ = G⁻¹ and
    Jᵀ A = [[R],[0]].
  • Full suite: 960 passed. _qr.py at 100% statement and branch coverage, 0
    partial.
  • ruff check, ruff format --check, mypy --strict, interrogate (100%
    floor): all pass.

Paper

docs/paper/quadprog.tex, §sec:invariance — the deletion passage previously
asserted in three lines that "no analogous single-transformation form exists". It
now gives the reason (a Householder annihilates all but one component of a
single vector; the bulge left by a removed column is a subdiagonal spread
across distinct columns, so no one reflection reaches it), records the BLAS
routing and the numbers above, and states the failed first measurement — the
paper already reports negative results in full, so this fits its existing habit.

It also adds a second, independent corroboration of Proposition 1 on the
deletion side. Proposition 1 implies the sign flip is unnecessary: using the
rotation outright merely replaces R by S_k R and J by J S. That variant
was implemented and verified — every iteration count reproduced, |R| and |J|
elementwise unchanged, only signs differing — then rejected, because it
measures no faster (the flip is one in-place pass) and forfeits the reflection's
symmetry, which is what lets one 2×2 serve both J's columns and R's rows.

Compiles clean under tectonic: no errors, no undefined references, no LaTeX
warnings.

Not touched

The performance table at quadprog.tex:583. I could not verify whether the
7.1× at n=700 moves. My box-constrained reconstruction showed zero deletions, but
ran 7.25 ms against the paper's 46 ms — plainly not the same problem set, so it
proves nothing. Editing that table off my own numbers would be worse than leaving
it. Expectation is that it is unchanged, but that is a guess and the table should
not rest on one.

The abstract's "Three findings." This is a 5–8% gain in the same BLAS-routing
family as finding one, not a fourth finding.

🤖 Generated with Claude Code

qr_delete's chase applies a 2x2 reflection to a pair of Q's columns once per
step. Spelled out in NumPy that allocates two n-vectors per call; BLAS rot
does the same work in place, writing through to Q's Fortran-ordered buffer.

BLAS has no 2x2 reflection, only the rotation, whose second output is the
exact negation of the reflection's -- so one sign flip, exact in IEEE,
recovers the reference's convention. The guard on contiguity is load-bearing
rather than defensive: handed a strided view, f2py copies and silently drops
the overwrite, which is the same hazard _reflect already handles for dger.

2.0-3.0x on the pairwise update, and 5-8% per solve on problems whose working
set churns; inert where no constraint is ever dropped. Iteration counts and
active sets are unchanged -- all 867 differential tests against the C
reference still pass, as do the QR invariants.

The paper records the finding, including why no single-transformation form
exists for the deletion, and that the rotation-without-the-flip variant was
implemented and verified against Proposition 1 before being rejected for
measuring no faster and forfeiting the reflection's symmetry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 18:00
@tschm
tschm merged commit a575a1c into main Aug 6, 2026
0 of 2 checks passed
@tschm
tschm deleted the perf_blas_rot_delete_chase branch August 6, 2026 18:20
@tschm
tschm removed the request for review from Copilot August 6, 2026 18:34
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.

1 participant