Zulip Chat Archive
Stream: mathlib4
Topic: Ring-normalization in linarith
Heather Macbeth (Nov 07 2024 at 01:42):
I noticed that the current implementation of linarith effectively ring-normalizes twice.
Example: suppose we are running the core method docs#Linarith.proveFalseByLinarith on the comparisons-with-zero y * (x + z) < 0
, 1 - y * x < 0
, - z * y < 0
.
First this list is sent through the method docs#Linarith.linearFormsAndMaxVar, which parses each LHS as a linear combination of monomials in atoms -- here the atoms are x
, y
, z
and the monomials which appear are 1, x * y
and y * z
, so we get (<, [0, 1, 1])
, (<, [1, -1, 0])
, (<, [0, 0, -1])
. This method is basically a custom, proof-free implementation of ring
within the linarith
tactic code.
Then later, once the oracle has returned the right combination of these inequalities to produce a contradiction, 1 * (y * (x + z)) + 1 * (1 - y * x) + 1 * (- z * y) < 0
, the tactic ring
is run directly on the LHS (to check that it ring-normalizes to zero).
Heather Macbeth (Nov 07 2024 at 01:42):
@Rob Lewis @Mario Carneiro Did you ever consider sharing more code between ring
and linarith
? One could use ring
directly for the parsing of each LHS in linearFormsAndMaxVar
, saving the proofs, and then adapt the final ring
call so that it used this information -- do you have an estimate of how much this might improve performance?
Mario Carneiro (Nov 07 2024 at 01:44):
this could be improved by having linarith call the ring internal function rather than the frontend on that contradiction
Heather Macbeth (Nov 07 2024 at 01:44):
(Cross-ref: Pretty sure this discussion between Rob and Mario is contemporary with the existing implementation)
Heather Macbeth (Nov 07 2024 at 01:49):
Mario Carneiro said:
this could be improved by having linarith call the ring internal function rather than the frontend on that contradiction
That makes sense, but I think what I was trying to propose is different? Namely, you could save the list of normalized LHSs as docs#Mathlib.Tactic.Ring.ExSum objects, and then the final normalization step only consists of a single layer of docs#Mathlib.Tactic.Ring.evalMul and a single layer of docs#Mathlib.Tactic.Ring.evalAdd.
Mario Carneiro (Nov 07 2024 at 01:53):
yes we're saying the same thing; skip the call to eval
and instead call the internal functions
Mario Carneiro (Nov 07 2024 at 01:54):
meaning that you would be storing atoms as ExProd
objects most likely (not ExSum
since linarith breaks down the additive structure of expressions)
Heather Macbeth (Nov 07 2024 at 01:55):
Ah, got it, I thought you were just proposing to switch from the current method (calling evalTactic (← `(tactic| ring1))
on a generated stuff = 0
goal) to directly calling docs#Mathlib.Tactic.Ring.eval ... which might indeed save a bit of time but not as much
Mario Carneiro (Nov 07 2024 at 01:56):
(I think they would also be monic monomials but ring doesn't have a dedicated type for that)
Mario Carneiro (Nov 07 2024 at 01:57):
It might be problematic that ring is strongly typed and linarith is not as much
Heather Macbeth (Nov 07 2024 at 01:57):
I think there's already a step where linarith groups hypotheses by their ambient type, so you could keep around the type information from that step onwards.
Heather Macbeth (Nov 07 2024 at 01:59):
Anyway ... any estimate of how much this deduplication might affect performance?
Mario Carneiro (Nov 07 2024 at 02:00):
not really, it could be significant but probably not too much
Mario Carneiro (Nov 07 2024 at 02:01):
There are some expensive steps in the part that would be skipped here, like defeq checking every subterm against every atom
Heather Macbeth (Nov 07 2024 at 02:02):
By "skipped here" do you mean in the "before" or the "after"?
Mario Carneiro (Nov 07 2024 at 02:02):
after
Mario Carneiro (Nov 07 2024 at 02:03):
the delta is the running of the match expression in eval
, which is moderately expensive because it calls into lean unification
Mario Carneiro (Nov 07 2024 at 02:04):
whether it dominates the run time or not depends a lot on the goal itself
Mario Carneiro (Nov 07 2024 at 02:04):
it can be the most expensive part if you have a lot of definitions that are expensive to compare
Heather Macbeth (Nov 07 2024 at 02:05):
It also has to ring-normalize twice, right? So if say (x + y) ^ 4
appears and it has to expand that out twice (once without proof and once with proof), shouldn't that also be expensive?
Mario Carneiro (Nov 07 2024 at 02:06):
ah that's true, ring doesn't cache subterm evaluation, maybe it should
Mario Carneiro (Nov 07 2024 at 02:07):
and doing this would indeed skip that cost
Heather Macbeth (Nov 07 2024 at 02:07):
But would that help? In the current implementation, the first time of expanding out is done by linarith's internal "ring-like" function, not by ring itself.
Mario Carneiro (Nov 07 2024 at 02:08):
well with your proposal linarith would call ring's normalization function on each atom early in the process
Mario Carneiro (Nov 07 2024 at 02:08):
on the other hand, maybe that's wasted work if the atom doesn't participate in the final contradiction
Heather Macbeth (Nov 07 2024 at 02:08):
Ah, good point!
Mario Carneiro (Nov 07 2024 at 02:09):
you could have a thunk there to compute it lazily but not recompute it
Heather Macbeth (Nov 07 2024 at 02:09):
At that point, I guess you have to re-implement ring
too, to be flexible to whether you want a proof-ful or proof-less ring call.
Mario Carneiro (Nov 07 2024 at 02:10):
how do you figure?
Heather Macbeth (Nov 07 2024 at 02:10):
Hmm, did I misunderstand your proposal?
Mario Carneiro (Nov 07 2024 at 02:11):
For each atom, you would store it as an expression as currently, but also store a function to compute it to an ExProd
on demand and cache the result
Mario Carneiro (Nov 07 2024 at 02:11):
or just work it out manually with an Option ExProd
Heather Macbeth (Nov 07 2024 at 02:12):
By "atom" you mean what linarith calls a "monomial", right? Like x^3 * y
or x * y * z
-- so an atom for linarith but not an atom for ring?
Mario Carneiro (Nov 07 2024 at 02:12):
yes
Heather Macbeth (Nov 07 2024 at 02:13):
I guess I was thinking that there are expensive parts even before you get to monomials: expanding (x + y) ^ 4
into monomials (even before normalizing those monomials) would be expensive, right?
Mario Carneiro (Nov 07 2024 at 02:13):
hm, when does that happen currently?
Mario Carneiro (Nov 07 2024 at 02:14):
Oh, I guess my assertion was false, linarith atoms are in general ExSum
not ExProd
because of things like that
Heather Macbeth (Nov 07 2024 at 02:15):
Heather Macbeth (Nov 07 2024 at 02:15):
I think linarith does the expanding out into "monomials" (which are effectively ExProd
s) and then runs the oracle using those as the atoms.
Mario Carneiro (Nov 07 2024 at 02:16):
but if you had (x + y) ^ 4
in a linarith goal, it would treat it as an atom, no?
Mario Carneiro (Nov 07 2024 at 02:16):
it would not be expanded until it got to ring
Mario Carneiro (Nov 07 2024 at 02:17):
and in fact it should probably never be expanded, since linarith has reason to believe it has a contradiction without expanding it
Heather Macbeth (Nov 07 2024 at 02:17):
No, IIUC it is treated as a linear combination of the atoms x^4, x^3y, x^2y^2, etc.
Heather Macbeth (Nov 07 2024 at 02:18):
import Mathlib
example (x y : ℝ) (h1 : (x + y) ^ 2 = 10) (h2 : (x - y) ^ 2 = 6) : x * y = 1 := by linarith
Mario Carneiro (Nov 07 2024 at 02:18):
interesting
Mario Carneiro (Nov 07 2024 at 02:19):
okay so I take back my assertion that my previous assertion was false
Mario Carneiro (Nov 07 2024 at 02:20):
it does make you wonder if that function couldn't just be eval
itself
Mario Carneiro (Nov 07 2024 at 02:21):
unfortunately linarith hasn't gotten much love in a while, it's pretty much just a direct port of the lean 3 version, while ring was a complete rewrite
Mario Carneiro (Nov 07 2024 at 02:22):
Now I am remembering that we had plans for a sum of squares tactic in bonn that didn't go anywhere :(
Heather Macbeth (Nov 07 2024 at 02:32):
Mario Carneiro said:
unfortunately linarith hasn't gotten much love in a while, it's pretty much just a direct port of the lean 3 version, while ring was a complete rewrite
Yes, I've been looking a bit more closely at it since I want to share some code between it and linear_combination
-for-inequalities. There are a few other places where I have (naive?) questions about the implementation -- like, for example, there's a lot of mkApp ....
and I wonder about switching all that stuff over to q(...)
.
Mario Carneiro (Nov 07 2024 at 02:34):
yes I would like to do that, it's just a lot of code and by the time it was ported it was too late to suggest starting afresh
Kim Morrison (Nov 07 2024 at 04:26):
Never too late to start afresh. :-)
Kim Morrison (Nov 07 2024 at 04:27):
I wish we didn't use identifying-atoms-up-to-defeq, which is potentially super slow, and just ran CanonM
.
Kim Morrison (Nov 07 2024 at 04:28):
Much better performance, and not much loss of capability.
Mario Carneiro (Nov 07 2024 at 04:29):
Why not make CanonM
have a reducibility flag? I'd be happy to switch the default but I don't want to sacrifice the capability
Kim Morrison (Nov 07 2024 at 04:46):
It does already, see CanonM.run'
.
Last updated: May 02 2025 at 03:31 UTC