Skip to content

Bound the resultant's elimination by measured work rather than by reasoned size (#921) - #927

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
perf/resultant-sylvester-ceiling
Aug 14, 2026
Merged

Bound the resultant's elimination by measured work rather than by reasoned size (#921)#927
Rafael-SOWNet merged 1 commit into
masterfrom
perf/resultant-sylvester-ceiling

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #921.

MaxSylvesterSize = 24 was arithmetic about a step count, not a measurement of anything, and the issue asked for the two axes that actually matter to be measured — Sylvester size, and terms per entry. They were, across 5 widths and 9 sizes, timing and allocation both.

The answer is that a bound on the input shape cannot do this job.

What was measured

Sylvester size against terms per coefficient, with the ceiling lifted so the shape past 24 is visible. Time and allocated bytes, one thread, warmed.

terms/entry size ms allocated outcome
1 40 31 35 MB answered
2 24 422 689 MB answered
2 40 2880 8.4 GB answered
3 16 838 2.9 GB answered
3 24 6248 21 GB declined
3 40 21980 79 GB declined
8 24 2233 8.4 GB declined

Three findings, none of them visible from the step count:

  1. Size is the cheap axis, and 24 was far too mean for it. With scalar entries the elimination does size^3/3 units of work; size 40 costs 31 ms and 35 MB.
  2. Cost is violent in terms per entry — between the fifth and the eighth power of it over the range — so no product of the two axes is the right law either.
  3. The most expensive input is not the widest. Entries wide enough trip MultivariatePolynomial.MaxTerms early and decline cheaply; three-term entries grow just slowly enough to spend 6 seconds and 21 GB before declining at size 24. The shape that costs the most is the one just inside the term ceiling, and nothing read off the degrees can see it.

What changed

MaxSylvesterSize 24 → 40. It now exists only to stop the matrix being built at all. 40 admits the discriminant of anything up to degree 20 rather than 12.

New MaxEliminationWork = 2_500_000 — a budget on the work done rather than the work predicted. Each step charges the product of the term counts of the operands of each of its two products, which is what a sparse multiplication costs, and it is charged before the multiplication so the budget cannot be overshot by the step that was going to be the worst.

Over the whole sweep that unit predicts both resources linearly and tightly — 1.4 KB of allocation and 0.4 µs per unit — so the budget is about a second and about three gigabytes, and moving it moves both together.

What it buys

before after
every refusal in the sweep up to 22 s and 79 GB 1.0–1.2 s and 3.5–4.2 GB
the sweep as a whole 2 m 50 s 37 s

Nothing that answered within the old ceiling stops answering, and cases that declined there decline up to six times faster. Above it the raise adds answers: scalar entries to size 40, two-term entries to size 32, the discriminant of a degree-16 polynomial.

The two costs, stated plainly. One case is refused that the sweep showed answering — two-term entries at size 40, 2.9 s and 8.4 GB — and the old ceiling refused that one too. And a large wide input now takes up to a second to be refused where a ceiling of 24 refused it for nothing; that is the price of no longer refusing the cheap ones along with it.

Tests

  • TheLargestAdmittedEliminationStillAnswers moves to the new ceiling: two degree-20 polynomials with roots 1..20 and 21..40, checked against the product over the differences of the roots. 35 ms.
  • InputTooLargeForTheEliminationIsRefusedRatherThanAttempted moves to size 42, one past the ceiling.
  • AnEliminationPastTheWorkBudgetIsRefusedThoughItsSizeIsAdmitted is new, and is the point of there being two bounds: three variables, two monomials per coefficient, size 40 — admitted by the ceiling and refused on work.

No BREAKING-CHANGES.md entry: PolynomialResultant is internal and nothing in the library calls it yet, which is also why #921 said this blocks nothing and is worth settling before something does.

Measured

Suite 6961 passed / 0 failed; F# wrapper 130/130; casbench 116/119 with 0 wrong, 0 error, 0 timeout; propcheck 1340 checks / 0 failures; rootcheck 596/596 clean; simpsweep 10463/10463 agree.

🤖 Generated with Claude Code

…soned size (#921)

MaxSylvesterSize = 24 was arithmetic about a step count, not a measurement of anything, and #921
asked for the two axes that actually matter to be measured: Sylvester size, and terms per entry.
They were, across 5 widths and 9 sizes, timing and allocation both, and the answer is that a bound
on the input shape cannot do the job.

    width  size  ms      allocated  outcome        (before this change; ceiling lifted to sweep)
        1    40      31       35 MB  answered
        2    24     422      689 MB  answered
        2    40    2880      8.4 GB  answered
        3    16     838      2.9 GB  answered
        3    24    6248       21 GB  declined
        3    40   21980       79 GB  declined
        8    24    2233      8.4 GB  declined

Three things follow, none of them visible from the step count.

**Size is the cheap axis and 24 was far too mean for it.** With scalar entries the elimination does
size^3/3 units of work; size 40 is 31ms and 35MB. The ceiling is now 40, which admits the
discriminant of anything up to degree 20 rather than 12. The largest admitted elimination -- two
degree-20 polynomials with roots 1..20 and 21..40, checked against the product over the differences
of the roots -- takes 35ms.

**Cost is violent in terms per entry**, between the fifth and the eighth power of it over the range,
so no product of the two axes is the right law either.

**And the most expensive input is not the widest**, which is what makes any bound read off the
degrees wrong in principle. Entries wide enough trip MultivariatePolynomial.MaxTerms early and the
elimination declines cheaply; three-term entries grow just slowly enough to spend 6 seconds and 21GB
before declining at size 24. The shape that costs the most is the one just inside the term ceiling.

So the second bound is on the work done rather than on the work predicted. Each step charges the
product of the term counts of the operands of each of its two products -- what a sparse
multiplication costs -- and the elimination stops past MaxEliminationWork = 2,500,000, charged
before the multiplication so the budget cannot be overshot by the step that was going to be the
worst. Over the whole sweep that unit predicts both resources linearly and tightly, 1.4KB of
allocation and 0.4us per unit, so the budget is about a second and about three gigabytes and moving
it moves both together.

    every refusal in the sweep     up to 22s and 79 GB  ->  1.0-1.2s and 3.5-4.2 GB
    whole sweep                                2m 50s   ->  37s

Nothing that answered within the old ceiling stops answering, and the cases that declined there
decline up to six times faster. Above it the raise adds answers: scalar entries to size 40, two-term
entries to size 32, the discriminant of a degree-16 polynomial. One case is refused that the sweep
showed answering -- two-term entries at size 40, 2.9 seconds and 8.4GB -- and the old ceiling
refused that one too. The cost of the raise is that a large wide input now takes up to a second to
be refused where a ceiling of 24 refused it for nothing; that is the price of no longer refusing the
cheap ones with it.

No BREAKING-CHANGES entry: PolynomialResultant is internal and nothing in the library calls it yet,
which is also why #921 said this blocks nothing and is worth settling before something does.

Measured: suite 6961 passed / 0 failed; F# wrapper 130/130; casbench 116/119 with 0 wrong, 0 error,
0 timeout; propcheck 1340 checks / 0 failures; rootcheck 596/596 clean; simpsweep 10463/10463 agree.

#921

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit e800a63 into master Aug 14, 2026
25 checks passed
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.

The resultant's Sylvester-size ceiling is reasoned about, not measured

1 participant