Zulip Chat Archive

Stream: general

Topic: Large-ish computation without Lean.ofReduceBool


Daniel Weber (Jul 27 2024 at 05:44):

I'm trying to prove the theorem that every consecutive sequence of up to 16 integers contains one which is coprime to all the others. It's fairly easy to show that it suffices to check up to the primorial of m, and this can simply be checked with native_decide:

import Mathlib.NumberTheory.Primorial
import Mathlib.Tactic.Peel

@[reducible]
def prop1 (m n : ) : Prop :=
   k < m,  v < m, v  k  Nat.Coprime (n + k) (n + v)

lemma l1 :  m < 16,  n < primorial m, prop1 (m + 1) n := by native_decide

lemma dvd_primorial_of_prime_of_le (p : ) (hp : Nat.Prime p) (n : ) (h : p  n) : p  primorial n := by
  apply Finset.dvd_prod_of_mem
  simp [hp]
  omega

lemma l2 {m n : } : prop1 m n  prop1 m (n % primorial (m-1)) := by
  apply not_iff_not.mp
  simp_all only [not_exists, not_and, not_forall, Classical.not_imp,
    Nat.Prime.not_coprime_iff_dvd]
  peel with x hx y hy hxy p hp
  wlog hxy : x < y generalizing x y
  · exact and_comm.trans <| this y hy x hx (by omega) (by omega) |>.trans and_comm
  by_cases this : p  y - x
  · replace : p  y - x := Nat.le_of_dvd (by omega) this
    replace : p  primorial (m - 1) := dvd_primorial_of_prime_of_le p hp (m-1) (by omega)
    replace : p  n - n % primorial (m - 1) := this.trans (Nat.dvd_sub_mod n)
    have hn : n % primorial (m - 1)  n := Nat.mod_le _ _
    apply and_congr
    repeat {
      convert (Nat.dvd_add_iff_left (n := n - n % primorial (m-1)) this).symm using 2
      omega
    }
  · apply iff_of_false
    repeat {
      intro nh
      apply this
      convert Nat.dvd_sub' nh.2 nh.1 using 1
      omega
    }

lemma l3 (m : ) (hm : m < 16) (n : ) : prop1 (m + 1) n :=
  l2.mpr <| l1 _ hm _ (Nat.mod_lt _ (primorial_pos _))

#print axioms l3

Is there a to get the kernel to check this, without assuming Lean.ofReduceBool? Using decide it can only check up to 7, but in theory for verifying it up to 16 you only need a check about a million equations of the form a*x + b*y = 1, so I think it could be possible.
Does it seem possible? How could I go about that?
(I can of course prove it with a more sophisticated case analysis, like in S. S. Pillai's article On M consecutive integers — I, but I'm curious if it could be avoided)

Kyle Miller (Jul 27 2024 at 17:05):

I tried having the kernel reduce the decidable instance itself, rather than pre-reducing it in the elaborator like decide does, but after dozens of seconds of computation, it gets a deep recursion error

@[reducible]
def prop1 (m n : ) : Prop :=
   k < m,  v < m, v  k  Nat.Coprime (n + k) (n + v)

open Lean Elab Tactic Meta in
/-- Trust that the statement is true, have the kernel check it. -/
elab "decide_true!" : tactic =>
  closeMainGoalUsing fun expectedType => do
    mkDecideProof expectedType

-- (kernel) deep recursion detected
lemma l1 :  m < 16,  n < primorial m, prop1 (m + 1) n := by
  decide_true!

Daniel Weber (Jul 27 2024 at 17:11):

Deciding Nat.Coprime is still a gcd calculation the kernel has to do, right? Would it be possible to compute coefficients for ax + by = 1 and directly use those?

Kyle Miller (Jul 27 2024 at 17:12):

Can you compute those faster than computing gcd? Mathlib uses the euclidean algorithm for gcd

Daniel Weber (Jul 27 2024 at 17:13):

No, but this computation doesn't have to occur in the kernel

Daniel Weber (Jul 27 2024 at 17:15):

Also, the kernel is still finding the correct value of k, right? Could that be moved outside of it?

Kyle Miller (Jul 27 2024 at 17:16):

I think the big recursion the kernel is doing is when it iterates up to primorial m

Kyle Miller (Jul 27 2024 at 17:16):

It gets up to 30030

Daniel Weber (Jul 27 2024 at 17:17):

Would making the decidable instance a tree help?

Kyle Miller (Jul 27 2024 at 17:19):

Here's something I did once to get deeper recursion: https://github.com/leanprover-community/mathlib4/blob/3b78a47b048ee856c6fa2bdf5c94dffb8b0223ad/Mathlib/NumberTheory/LucasLehmer.lean#L554-L573

Kyle Miller (Jul 27 2024 at 17:21):

There's also manually handling each top-level case, but I'm not sure how to keep going with the last five:

lemma l1 :  m < 16,  n < primorial m, prop1 (m + 1) n := by
  intro m hm
  interval_cases m
  · decide
  · decide
  · decide
  · decide
  · decide
  · decide
  · decide
  · set_option maxRecDepth 1000 in decide
  · set_option maxRecDepth 1000 in decide
  · set_option maxRecDepth 1000 in decide
  · set_option maxRecDepth 1000 in decide
  all_goals intro n hn; conv at hn => rhs; reduce
  -- five more goals

Daniel Weber (Jul 27 2024 at 17:45):

this seems to prevent the recursion depth error, but it simply takes forever

attribute [-instance] Nat.decidableLoHi

@[semireducible]
instance decidableLoHi' (lo hi : ) (P :   Prop) [H : DecidablePred P] :
    Decidable ( x, lo  x  x < hi  P x) := by
  by_cases h : lo < hi
  · by_cases h₂ : lo + 1 = hi
    · apply decidable_of_iff (P lo)
      constructor
      · intro h x hx1 hx2
        have : x = lo := by omega
        rwa [this]
      · intro h
        apply h <;> trivial
    · have t1 := decidableLoHi' lo ((lo + hi)/2) P
      have t2 := decidableLoHi' ((lo + hi)/2) hi P
      have := @instDecidableAnd _ _ t1 t2
      apply @decidable_of_iff _ _ _ this
      constructor
      · intro h x hx1 hx2
        cases Nat.lt_or_ge x ((lo + hi)/2)
        · apply h.1 <;> omega
        · apply h.2 <;> omega
      · intro h
        constructor
        · intro x hx1 hx2
          apply h <;> omega
        · intro x hx1 hx2
          apply h <;> omega
  · apply Decidable.isTrue
    intros
    omega

lemma l1 :  m < 16,  n, (0  n)  (n < primorial m)  prop1 (m + 1) n := by
  decide_true!

Daniel Weber (Sep 10 2024 at 13:43):

I managed to get this to work up to 12 (with a slightly heavy rfl), do you think there's a way to push it up to 16? Perhaps List.forall_ind could be made more kernel-friendly?

import Mathlib.NumberTheory.Primorial
import Mathlib.Tactic.Peel
import Mathlib.Tactic.Eval
import Mathlib.Tactic.IntervalCases
import Mathlib.Data.Int.GCD

@[reducible]
def prop1 (m n : ) : Prop :=
   k < m,  v < m, v  k  Nat.Coprime (n + k) (n + v)

def List.forall_ind {α : Type*} :   (List α)  (  α  Bool)  Bool
| _, [], _ => true
| i, a :: r, f => bif (f i a) then List.forall_ind (i + 1) r f else false

theorem List.forall_iff {α : Type*} {i : } (l : List α) {f :   α  Bool} :
    List.forall_ind i l f   j (hj : j < l.length), f (j + i) l[j] := by
  induction l generalizing i with
  | nil => simp [forall_ind]
  | cons a r hind =>
    simp [forall_ind, hind]
    conv => rhs; rw [ Nat.and_forall_add_one]
    simp [add_comm,  add_assoc]

def prop2 (m n : ) (k :  × List ( × )) : Bool :=
  have k, l := k
  (k < m) &&
    l.length == m-1 &&
    l.forall_ind 0 (fun i a, b  (n + (k + 1 + i) % m) * a + (n + k) * b = 1)

partial def generateList (m n : ) :  × (List ( × )) :=
  generateList.go m n 0 0 []
  where
  generateList.go (m n k i : ) (acc : List ( × )) :  × (List ( × )) :=
    if i = m - 1 then
      (k, acc.reverse)
    else
    let a, b := Nat.xgcd (n + (k + 1 + i) % m) (n + k)
    if a*(n + (k + 1 + i) % m)+b*(n + k) = 1 then
      generateList.go m n k (i + 1) ((a, b) :: acc)
    else
      generateList.go m n (k + 1) 0 []

partial def generateListList (m n : ) : List ( × (List ( × ))) :=
  (generateListList.go m n 0 []).reverse
  where
  generateListList.go (m n i : ) (aux) :=
    if i = n then
      aux
    else
      generateListList.go m n (i + 1) (generateList m i :: aux)

partial def generateListListList (b : ) (n :   ) : List (List ( × (List ( × )))) :=
  (generateListListList.go b 0 []).reverse
  where
  generateListListList.go (b i : ) (aux) : List (List ( × (List ( × )))) :=
    if i = b then
      aux
    else
      generateListListList.go b (i + 1) (generateListList (i + 1) (n i) :: aux)

theorem forall_forall_lt_prop1_iff (b : ) (n :   ) :
        ( m < b,  i < n m, prop1 (m + 1) i)   a : List (List ( × (List ( × )))),
    (a.length == b) && (a.forall_ind 0 (fun i k  k.length == n i &&
      k.forall_ind 0 (fun j k, l 
      k < (i + 1) &&
      l.length == i &&
      l.forall_ind 0 (fun i2 a, b  (j + (k + 1 + i2) % (i + 1)) * a + (j + k) * b == 1)
      ))) := by
  sorry

def test (v : ) : Bool :=
  have a := generateListListList v primorial
  (a.length == v) && (a.forall_ind 0 (fun i k  k.length == primorial i &&
      k.forall_ind 0 (fun j k, l 
      k < (i + 1) &&
      l.length == i &&
      l.forall_ind 0 (fun i2 a, b  (j + (k + 1 + i2) % (i + 1)) * a + (j + k) * b == 1)
      )))

#eval test 16

lemma l1 :  m < 12,  n < primorial m, prop1 (m + 1) n := by
  simp only [forall_forall_lt_prop1_iff]
  use eval% generateListListList 12 primorial
  count_heartbeats rfl

Joachim Breitner (Sep 11 2024 at 08:10):

Daniel Weber said:

Perhaps List.forall_ind could be made more kernel-friendly?

Maybe try to define it with Nat.rec directly, to avoid the Nat.brecOn overhead. Just a blind guess, but curious if will help. Is that really the bottleneck?

Daniel Weber (Nov 02 2024 at 06:53):

It reduced about 31M heartbeats (out of 391M), using

def List.forall_ind {α : Type*} (l : List α) (f :   α  Bool) :   Bool :=
  l.rec (fun _  true) (fun x _ hind i  (f i x).rec false (hind (i + 1)))

Joachim Breitner (Nov 02 2024 at 09:00):

Right, I've observed this 10x improvement in some cases as well, and am working on tooling that can maybe help achieving that (https://github.com/leanprover/lean4/issues/5806, which you already found), although it's unclear how convenient and universally applicable it will end up being.

Daniel Weber (Nov 02 2024 at 09:00):

It's not a 10x reduction, it's approximately a 10% reduction

Daniel Weber (Nov 02 2024 at 09:01):

Does the kernel know how to work with UInt more efficiently than Nat?

Joachim Breitner (Nov 02 2024 at 09:07):

Ah, misread your comment. I've seen 10x in some cases (e.g. our finite magma calculations)

Joachim Breitner (Nov 02 2024 at 09:08):

No, UInt are just slower Nat, and Array are just slower List. It's really no fun to write code for kernel reduction.

Daniel Weber (Nov 02 2024 at 09:09):

Using Nat.succ instead of i + 1 removed a few hundred thousand heartbeats

Joachim Breitner (Nov 02 2024 at 09:12):

Right. Getting rid of Fin (of it is involved) also helps a lot.
See https://github.com/teorth/equational_theories/pull/477 for an attempt to automate some of these transformations. Doing it always is not effective, but if you have single very slow proofs maybe it helps already.

Daniel Weber (Nov 02 2024 at 09:55):

Even when using native decide, though, it's much slower then I would've expected - most of that seems to be because docs#Nat.decidableBallLT isn't implemented tail-recursively


Last updated: May 02 2025 at 03:31 UTC