Skip to content

A14b: quick-wins batch 2 (#168 J zero-fill, #124 materialize guard) - #171

Merged
mmelnich merged 5 commits into
mainfrom
a14b-issue-backlog-quick-wins
Aug 27, 2026
Merged

A14b: quick-wins batch 2 (#168 J zero-fill, #124 materialize guard)#171
mmelnich merged 5 commits into
mainfrom
a14b-issue-backlog-quick-wins

Conversation

@mmelnich

@mmelnich mmelnich commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Second quick-wins batch from the A14 issue-backlog pass, following PR #148.

Closes #168, closes #124.

Summary

#168 is the silent-wrong-answer one: geqp3 reads jpvt on entry, and neither CQRRPT driver zeroed the caller's J. The audit found the GPU driver carries the identical defect, which the issue did not mention. BQRRP was already safe, but through a fill whose comment disclaimed it as probably unnecessary; that comment is corrected.

#124 is addressed in two parts. The generic materialize fallback still forms A * I, but the hidden n-by-n identity allocation is now capped (16384 columns, 2 GiB in double) and fails loudly past that. The type-specific path Riley recommended now covers all three concrete operator types: DenseLinOp and SparseLinOp already materialized by direct copy, and this PR adds the missing CompositeOperator overload, which materializes each operand through its own dispatch and forms the product with one gemm. That path builds no identity, carries no cap, and never routes through the composite's operator(), so it is also valid for correctness tests of operator() itself.

Changes

  • rl_cqrrpt.hh, rl_cqrrpt_gpu.hh: zero the caller's J before geqp3 (CQRRPT silently depends on J being zero-initialized (geqp3 reads jpvt on entry) #168).
  • rl_bqrrp.hh: correct the comment on the (already safe) J fill.
  • rl_materialize.hh: cap the identity allocation in the generic fallback; add the CompositeOperator overload (Operators materialization caution #124).
  • rl_test_utils.hh: materialize_linop docstring rewritten to describe the actual dispatch and where the circularity caution still applies.
  • test/drivers/test_cqrrpt.cc, test_cqrrpt_gpu.cu: regression tests running the same rank-deficient factorization with clean and dirtied J buffers, CPU and GPU.
  • test/linops/test_linops.cc: guard test for the cap; composite-vs-reference product test; wide-composite test proving the overload is exempt from the cap.

LAPACK's geqp3 reads jpvt on entry: a nonzero entry marks that column as
fixed and moves it to the front of the permutation. Neither CQRRPT driver
zeroed the caller's J, so the buffer's prior contents silently steered the
pivoting. In-tree callers pass a value-initialized std::vector and satisfy
the precondition by accident, which is why every existing test stayed green;
a binding layer handing over an uninitialized buffer saw the detected rank
wander between runs at a fixed RNG state.

Fixed in rl_cqrrpt.hh and in rl_cqrrpt_gpu.hh, which carries the identical
line (its QRCP runs on the host). BQRRP was already safe, but via the
std::fill at rl_bqrrp.hh:327 rather than its std::iota; corrected the
comment there, which called that fill 'may not be necessary' when it is
load-bearing for this same reason.

Regression test in each language of the pair: a rank-deficient input
(m=2000, n=50, true rank 40) factored three times at one RNG state, with J
clean and then dirtied two different ways. Detected rank must be identical
across trials. Fails on main (rank 40 then 41), passes here.
…#124)

The generic materialize path forms buf = A * I, allocating an n-by-n
identity behind the caller's back. Nothing bounded it, so materializing a
wide operator could request an enormous hidden allocation with no
diagnostic. Guard on a named constant (MATERIALIZE_IDENTITY_MAX_DIM =
16384, which is 2 GiB in double) using the randlapack_require idiom already
used in this file, and point the error text at the type-specific overloads.

Those overloads (DenseLinOp, SparseLinOp, CompositeOperator) do not form an
identity and are deliberately left uncapped; they are the answer for
operators too large for the fallback. Riley's original objection on this
issue, that linop correctness tests should not prove an operator by
applying it to the identity, is already addressed by those overloads
existing.
…romised

The cap message and the test-utils docstring both pointed callers at a
CompositeOperator overload that did not exist, so a composite wider than
the cap had no direct path. The overload materializes each operand through
its own dispatch and forms the product with one gemm: no identity, no cap,
and no call through the composite's operator(), which also makes it safe
for correctness tests of operator() itself. Docstrings updated to match.
Comments now state only the technical constraint in a few lines; the
provenance and full rationale live in the PR body and commit history.
@mmelnich
mmelnich merged commit f9134bc into main Aug 27, 2026
12 checks passed
@mmelnich
mmelnich deleted the a14b-issue-backlog-quick-wins branch August 27, 2026 21:18
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.

CQRRPT silently depends on J being zero-initialized (geqp3 reads jpvt on entry) Operators materialization caution

1 participant