Skip to content

fix(qd_cascade): compress the expansion before renormalizing addition - #1321

Merged
Ravenwater merged 2 commits into
mainfrom
fix/1317-qd-cascade-expansion-compression
Aug 16, 2026
Merged

fix(qd_cascade): compress the expansion before renormalizing addition#1321
Ravenwater merged 2 commits into
mainfrom
fix/1317-qd-cascade-expansion-compression

Conversation

@Ravenwater

@Ravenwater Ravenwater commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Closes #1317.

qd_cascade sqrt/exp/log delivered 48-50 correct decimal digits where the format carries
63.6 and classic qd delivers all of them. The cause was in none of those functions.

Root cause

expansion_ops::add_cascades() merges the operands' components by magnitude, accumulates them
with two_sum, and returns the running sum plus the collected errors. The value of that
expansion is exact, but its components overlap - consecutive terms can be within a factor of
two of each other rather than a factor of 2^53:

e[0] = 1.743e+00
e[1] = 1.110e-16     <- e[2] is 3x smaller, not 2^53x smaller
e[2] = 3.385e-17
e[3] = 1.233e-32

compress_8to4() is the QD library's renorm, built entirely from fast_two_sum chains, and
fast_two_sum(a, b) is only error-free when |a| >= |b|. Handed an overlapping expansion it runs
out of places to put the tail and returns a result whose fourth component is exactly zero - a
relative error of 2^-160 in a format carrying 2^-212, on roughly one addition in eight.

Everything built on addition inherited it, which is why it surfaced in the 16 squarings of exp()
rather than in addition's own test suite. The existing suites all check self-consistency, and a
result that is normalized, round-trips, and matches the other implementation of the same flawed
algorithm passes every one of them.

Fix

expansion_ops::compress_expansion<M>() - Shewchuk's COMPRESS (Geometric Predicates, Fig. 22):
a downward sweep carrying each rounding remainder into the next component, then an upward sweep
leaving the leading components correctly rounded. It is value-preserving; it changes only the
shape of the expansion, which is exactly what the precondition is about.

Applied to the floatcascade<4> overload only. The <2> and <3> overloads were measured
over 5000 random full-precision additions each and stay within 0.49 ulp of their own format with
and without it - fewer merged terms compressed into fewer output components leaves enough slack
that the overlap never reaches a surviving component. Compressing them anyway would have doubled
dd_cascade addition for no accuracy gain, so both carry a comment recording that.

Verification

Independent oracle (Python decimal at 140 digits, sharing no code with the library), 200 random
full-precision operand pairs, worst relative error:

operation before after classic qd
add 1.7e-49 exact 6.2e-65
sqrt 1.3e-49 6.1e-64 5.5e-65
exp 4.8e-51 4.0e-65 4.0e-65
log 1.3e-50 2.3e-63 2.3e-63

exp and log now match qd to the last bit.

Cost, and a reversed conclusion

Quad-double addition is 46% more expensive (48.4 -> 70.4 nsec/op). That reverses one of the #1315
benchmark's findings: qd_cascade's dot product measured 0.83x qd only because the addition
was dropping a component
; with the correct addition it is 1.15x. The benchmark README is
re-measured and its tables and conclusions updated accordingly - including the "can we retire
classic qd" answer, whose accuracy blocker is now gone and whose remaining blockers are purely
speed.

Tests

static/highprecision/{qd,td}_cascade/arithmetic/addition_oracle.cpp validate addition against
exact dyadic-rational arithmetic (dyadic_exact.hpp, backed by einteger) - no shared code with
the expansion arithmetic under test, so it cannot be fooled by a result that is well-formed and
wrong. The qd_cascade suite fails 825 of 4096 cases at REGRESSION_LEVEL_4 on the old code
and passes on the new; the td_cascade suite guards the overload deliberately left uncompressed.

Also corrects the 2026-03-16 PRECISION NOTE in qd_cascade/exponent.hpp, which attributed this
loss to multiply_cascades(). Multiplication is ~2 digits behind qd's on full-width operands
(1.7e-63 vs 1.6e-65) - real, inside the format, and worth tracking separately.

113 dd/qd/cascade regression tests pass under gcc 13.3.0 and clang 18.1.3.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved numerical stability and accuracy for high-precision cascade arithmetic, particularly during addition and exponential calculations.
    • Corrected documented accuracy results and clarified remaining precision limitations.
  • Tests

    • Added independent, exact-arithmetic validation for double-double and quad-double calculations.
    • Expanded regression coverage for addition and square-root accuracy.
  • Documentation

    • Updated high-precision benchmark results and performance guidance to reflect the latest behavior and tradeoffs.

…#1317)

qd_cascade sqrt/exp/log delivered 48-50 correct decimal digits where the
format carries 63.6 and where classic qd delivers all of them. The cause was
in none of those functions.

expansion_ops::add_cascades() merges the two operands' components by
magnitude, accumulates them with two_sum, and returns the running sum plus
the collected errors. The VALUE of that expansion is exact, but its
components overlap - consecutive terms can be within a factor of two of each
other rather than a factor of 2^53:

    e[0] = 1.743e+00
    e[1] = 1.110e-16     <- e[2] is 3x smaller, not 2^53x smaller
    e[2] = 3.385e-17

compress_8to4() is the QD library's renorm, built entirely from fast_two_sum
chains, and fast_two_sum is only error-free when |a| >= |b|. Handed an
overlapping expansion it runs out of places to put the tail and returns a
result whose fourth component is exactly zero. That is a relative error of
2^-160 in a format carrying 2^-212, on roughly one addition in eight, and
everything built on addition inherited it - which is why it surfaced in the
16 squarings of exp() rather than in addition's own test suite.

The fix is expansion_ops::compress_expansion<M>(), Shewchuk's COMPRESS
(Geometric Predicates, Fig. 22): a downward sweep carrying each rounding
remainder into the next component, then an upward sweep leaving the leading
components correctly rounded. It is value-preserving - it changes only the
SHAPE of the expansion, which is exactly what the precondition is about.

Applied to the floatcascade<4> overload only. The <2> and <3> overloads were
measured over 5000 random full-precision additions each and stay within 0.49
ulp of their own format with and without it: fewer merged terms compressed
into fewer components leaves enough slack that the overlap never reaches a
surviving component. Compressing them anyway would have doubled dd_cascade
addition for no accuracy gain, so both carry a comment saying so.

Verified against an exact oracle (Python decimal at 140 digits, no shared
code), 200 random full-precision operand pairs, worst relative error:

                    before      after    classic qd
      add          1.7e-49      exact       6.2e-65
      sqrt         1.3e-49    6.1e-64       5.5e-65
      exp          4.8e-51    4.0e-65       4.0e-65
      log          1.3e-50    2.3e-63       2.3e-63

exp and log now match qd to the last bit.

Cost: quad-double addition is 46% more expensive (48.4 -> 70.4 nsec/op), and
that reverses one of the #1315 benchmark's conclusions - qd_cascade's dot
product was 0.83x qd only because the addition was dropping a component; with
the correct addition it is 1.15x. The benchmark README is re-measured and its
conclusions updated accordingly.

Tests: static/highprecision/{qd,td}_cascade/arithmetic/addition_oracle.cpp
validate addition against exact dyadic-rational arithmetic (dyadic_exact.hpp,
backed by einteger), which shares no code with the expansion arithmetic. The
qd_cascade suite fails 825 of 4096 cases at REGRESSION_LEVEL_4 on the old
code and passes on the new. The td_cascade suite is a guard for the overload
deliberately left uncompressed.

Also corrects the 2026-03-16 PRECISION NOTE in qd_cascade/exponent.hpp, which
attributed this loss to multiply_cascades(). Multiplication is ~2 digits
behind qd's on full-width operands (1.7e-63 vs 1.6e-65) - real, inside the
format, and tracked separately.

113 dd/qd/cascade regression tests pass under gcc 13.3 and clang 18.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 380b0ad7-4938-405a-8a2c-8374c1f8a388

📥 Commits

Reviewing files that changed from the base of the PR and between 0b8da42 and 40fa559.

📒 Files selected for processing (6)
  • benchmark/performance/arithmetic/highprecision/README.md
  • include/sw/universal/internal/floatcascade/floatcascade.hpp
  • include/sw/universal/number/qd_cascade/compress_8_4_bug.md
  • include/sw/universal/number/qd_cascade/math/functions/exponent.hpp
  • static/highprecision/qd_cascade/arithmetic/addition_oracle.cpp
  • static/highprecision/td_cascade/arithmetic/addition_oracle.cpp

📝 Walkthrough

Walkthrough

The change adds two-pass compression for floatcascade<4> additions, introduces exact-oracle regression tests for qd_cascade and td_cascade, and updates benchmark and precision documentation with post-fix results.

Changes

qd_cascade correctness

Layer / File(s) Summary
Four-component expansion compression
include/sw/universal/internal/floatcascade/floatcascade.hpp, include/sw/universal/number/qd_cascade/compress_8_4_bug.md
Adds compress_expansion and applies it to floatcascade<4> addition results. The documentation records why compression is not applied to smaller cascades.
Exact-oracle regression validation
static/highprecision/qd_cascade/arithmetic/addition_oracle.cpp, static/highprecision/td_cascade/arithmetic/addition_oracle.cpp
Adds deterministic exact-dyadic checks for addition and square-root residuals, with manual and regression execution modes.
Post-fix accuracy and performance records
benchmark/performance/arithmetic/highprecision/README.md, include/sw/universal/number/qd_cascade/math/functions/exponent.hpp
Updates benchmark measurements, residuals, conclusions, and precision notes after the compression fix.

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

Possibly related issues

Possibly related PRs

Suggested labels: bug

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1317-qd-cascade-expansion-compression

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

@coveralls

coveralls commented Aug 15, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31918517357

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-0.03%) to 85.5%

Details

  • Coverage decreased (-0.03%) from the base build.
  • Patch coverage: 27 of 27 lines across 1 file are fully covered (100%).
  • 31 coverage regressions across 3 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

31 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
include/sw/universal/internal/floatcascade/floatcascade.hpp 22 83.0%
include/sw/universal/number/td_cascade/manipulators.hpp 8 83.33%
include/sw/universal/number/td_cascade/td_cascade_impl.hpp 1 78.33%

Coverage Stats

Coverage Status
Relevant Lines: 49820
Covered Lines: 42596
Line Coverage: 85.5%
Coverage Strength: 7285584.31 hits per line

💛 - Coveralls

@Ravenwater Ravenwater self-assigned this Aug 16, 2026
@Ravenwater Ravenwater added this to the V4 milestone Aug 16, 2026
@Ravenwater
Ravenwater merged commit d1ccc76 into main Aug 16, 2026
23 of 25 checks passed
@Ravenwater
Ravenwater deleted the fix/1317-qd-cascade-expansion-compression branch August 16, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

qd_cascade sqrt/exp/log lose 13-15 decimal digits relative to qd (and are 2-6x slower)

2 participants