Bound the resultant's elimination by measured work rather than by reasoned size (#921) - #927
Merged
Merged
Conversation
…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>
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.
Closes #921.
MaxSylvesterSize = 24was 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.
Three findings, none of them visible from the step count:
size^3/3units of work; size 40 costs 31 ms and 35 MB.MultivariatePolynomial.MaxTermsearly 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
MaxSylvesterSize24 → 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
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
TheLargestAdmittedEliminationStillAnswersmoves 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.InputTooLargeForTheEliminationIsRefusedRatherThanAttemptedmoves to size 42, one past the ceiling.AnEliminationPastTheWorkBudgetIsRefusedThoughItsSizeIsAdmittedis 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.mdentry:PolynomialResultantisinternaland 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