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):

https://github.com/leanprover-community/mathlib4/blob/e8818a7d64e68e8ecc8c15bafc7964cb471fd2fb/Mathlib/Tactic/Linarith/Parsing.lean#L158

Heather Macbeth (Nov 07 2024 at 02:15):

I think linarith does the expanding out into "monomials" (which are effectively ExProds) 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