Zulip Chat Archive
Stream: lean4
Topic: Reduction of terms with free variables is expensive
Yann Herklotz (Sep 10 2024 at 12:35):
There seems to be a significant difference between the reduction strategies when using rfl
on expressions with and without free variables, but I am a bit confused why that is. I also can't come up with a completely standalone example of this (so I'm using a reduced example that uses Batteries.RBMap
, because examples that are too small don't seem to exhibit the same issue.
Below the proof of equality, which reduces the term, takes 500ms, and becomes exponentially longer the more elements are added to the first tree. Here T
is a fvar in the local context.
import Lean
import Batteries
set_option profiler true in -- takes 500ms (uncomment and it takes 7s)
example (T : Nat): (((Batteries.RBMap.ofList [(1, T), (2, T), (3, T) /-, (4, T), (5, T)-/] Ord.compare).mergeWith (λ _ a _ => a)
(Batteries.RBMap.ofList [(1, T), (2, T)] Ord.compare)).find? 1) = some T := by rfl
What I find strange is that just making T
a declaration, so that it becomes a constant in the expression, makes the proof of equality nearly instant.
import Lean
import Batteries
axiom T : Nat
set_option profiler.threshold 1 -- otherwise no time is shown
set_option profiler true in -- takes 8ms
example : (((Batteries.RBMap.ofList [(1, T), (2, T), (3, T), (4, T), (5, T)] Ord.compare).mergeWith (λ _ a _ => a)
(Batteries.RBMap.ofList [(1, T), (2, T)] Ord.compare)).find? 1) = some T := by rfl
In our proofs we seem to be hitting this inefficiency quite often, because we are mainly referring to fvars in expressions. I would also not expect the reduction to be this slow, and I am not sure which exact step is taking so long. Does anyone have any insights on why this might be? Or if this is an expected behaviour of the reduction?
Kyle Miller (Sep 10 2024 at 15:29):
Maybe you're seeing a feature in the eq_refl
tactic (which is what rfl
for equalities), where if there are no free variables or metavariables, it uses kernel defeq instead of the usual elaborator defeq when validating that Eq.refl
can prove the goal. Kernel defeq is faster.
Kyle Miller (Sep 10 2024 at 15:33):
I'm not sure why docs#Lean.MVarId.refl doesn't allow fvars at all, since docs#Lean.Kernel.isDefEq does accept a local context. Though, it would need to instantiate all mvars in the local context and check that no mvars are remaining.
Yann Herklotz (Sep 10 2024 at 15:35):
Ah yes, that probably explains the difference between the free variable vs constant version. But I still wasn't expecting there to be a difference of 7s vs 8ms!
And thank you for the pointers, that's interesting that it could maybe be passed to the kernel defeq check. Instantiating all mvars in the local context should be fine as well.
Kyle Miller (Sep 10 2024 at 15:42):
If you can do some metaprogramming, it wouldn't be too difficult to make a ker_rfl
tactic.
One approach to make a local context would be to use mkForallFVars
on the target with usedOnly := true
to make a closed term, instantiate mvars at that point (and check that there are no mvars), then with an empty local context use forallTelescope
. This is all a way to remove false dependencies on mvars while instantiating them.
Yann Herklotz (Sep 10 2024 at 15:49):
Oh great thanks, yeah that shouldn't be too difficult, I'll try that out. And thanks for the pointer on usedOnly
, that should be useful!
Is there any point in trying to reduce the example further to see what takes 7s in the non kernel reduction? Or should one be avoiding the normal reduction in elaboration as much as possible?
Yann Herklotz (Oct 02 2024 at 13:36):
Just wanted to note that I've filed an issue about the eq_refl
performance on this term with free variables, because I feel like the the exponential number of functions that are unfolded as the size of the list increases is unexpected lean#5596.
Eric Wieser (Nov 22 2024 at 18:20):
Kyle Miller said:
it uses kernel defeq instead of the usual elaborator defeq when validating that
Eq.refl
can prove the goal.
Is this still the case, or does it now use elaborator defeq?
Kyle Miller (Nov 22 2024 at 18:40):
Just checked, no it does not use the kernel anymore.
Kyle Miller (Nov 22 2024 at 18:41):
Eric Wieser (Nov 22 2024 at 18:50):
Is there a version of rfl
that does use the kernel?
Eric Wieser (Nov 22 2024 at 18:51):
I came up with
run_tac do
let g ← Lean.Elab.Tactic.popMainGoal
let_expr Eq α a b := ← g.getType' | failure
g.assign (← Lean.Meta.mkEqRefl a)
but it wouldn't surprise me if something is wrong here
Kyle Miller (Nov 22 2024 at 18:53):
If it's applicable, you could use decide
's kernel mode (on nightly it's decide +kernel
), which is a bit less efficient than just using Eq.refl
Kyle Miller (Nov 22 2024 at 18:53):
I think the only thing that's wrong with what you have is that it doesn't do any validation at tactic time that the tactic succeeds. The decide +kernel
tactic will call out to the kernel at tactic time. It caches the result as a theorem too, so it doesn't need to be checked again.
Eric Wieser (Nov 22 2024 at 18:54):
That's decide!
in non-nightly versions?
Kyle Miller (Nov 22 2024 at 18:54):
Yeah
Eric Wieser (Nov 22 2024 at 18:56):
Hmm, that doesn't exist in the stable version of Lean it seems
Eric Wieser (Nov 22 2024 at 18:57):
Only in the release candidates
Eric Wieser (Nov 22 2024 at 18:58):
Kyle Miller said:
I think the only thing that's wrong with what you have is that it doesn't do any validation at tactic time that the tactic succeeds
What would that validation look like?
Kyle Miller (Nov 22 2024 at 18:59):
You'd want to ask the kernel to check that a
and b
are defeq before assigning that metavariable, and if that fails then throw an error
Kyle Miller (Nov 22 2024 at 18:59):
(using the code that 3772 removed)
Kyle Miller (Nov 22 2024 at 19:02):
Or, you could look at how decide +kernel
works. It uses the auxdecl interface to add a theorem to the environment.
Though, docs#Lean.Meta.mkAuxTheorem seems very convenient. It will auto-abstract the any fvars for you and create a theorem checked by the kernel, which you should be able to apply (though you'd have to be careful with transparency settings to make sure it doesn't trigger any expensive defeqs!)
Eric Wieser (Nov 22 2024 at 19:10):
I don't need anything sophisticated here, the purpose is to have a test that forces the kernel to crash
Eric Wieser (Nov 22 2024 at 19:11):
The test in question is
import Lean
/-- Check if the kernel would accept `rfl` -/
elab "rfl!?" : tactic => do
let g ← Lean.Elab.Tactic.getMainGoal
let_expr Eq α a b := ← g.getType' | failure
guard <| ← Lean.ofExceptKernelException <| Lean.Kernel.isDefEq (← Lean.getEnv) {} a b
def npowRec : Nat → Nat → Nat
| 0, _ => 1
| n + 1, a => npowRec n a * a
theorem foo : npowRec 2000 (npowRec 2000 2001) = npowRec 2000 (npowRec 20001 2000) := by
rfl!?
which indeed crashes / consumes all memory
Eric Wieser (Nov 22 2024 at 19:12):
(though arguably that's my fault for circumventing the elaborator)
Last updated: May 02 2025 at 03:31 UTC