Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: run hint tactics in parallel #8435

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
32e7f90
feat: the 'hint' tactic
semorrison Nov 12, 2023
b00f84d
imports
semorrison Nov 12, 2023
446f173
linting
semorrison Nov 12, 2023
a4c98a4
linting
semorrison Nov 12, 2023
f23ebb5
linter: relax, dude
semorrison Nov 12, 2023
3fa0c62
Merge branch 'master' into hint_tactic
Komyyy Nov 14, 2023
8f09de2
integrate
Komyyy Nov 14, 2023
90c6dfe
merge; enable aesop
semorrison Nov 14, 2023
69868ae
trickery for monadic tasks
semorrison Nov 14, 2023
a3daba0
Merge branch 'monadic_tasks' into hint_tactic_parallel
semorrison Nov 14, 2023
cbda2f7
kawow
semorrison Nov 14, 2023
6dfdb9d
restore module doc
semorrison Nov 15, 2023
ec48fd0
remove #align_import
semorrison Nov 15, 2023
d6a2989
use guard_msgs
semorrison Nov 15, 2023
0730fb5
Merge remote-tracking branch 'origin/master' into monadic_tasks
semorrison Nov 15, 2023
7d63f4e
cleanup
semorrison Nov 15, 2023
1d37b33
.
semorrison Nov 15, 2023
67fa640
Merge branch 'hint_tactic' into hint_tactic_parallel
semorrison Nov 15, 2023
d1a7db2
working
semorrison Nov 15, 2023
1ac08bd
merge master
semorrison Nov 15, 2023
cc64973
.
semorrison Nov 15, 2023
2105ad9
Merge remote-tracking branch 'origin/master' into monadic_tasks
semorrison Nov 16, 2023
5f16406
cleanup
semorrison Nov 16, 2023
c1883f3
Merge branch 'monadic_tasks' into hint_tactic_parallel
semorrison Nov 16, 2023
8d6cd12
cleanup
semorrison Nov 16, 2023
3324e07
merge master
semorrison Nov 16, 2023
10ba3e9
import all
semorrison Nov 16, 2023
d1aaaa2
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Nov 16, 2023
50828c6
non-deterministic messages
semorrison Nov 16, 2023
a2efb42
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Nov 19, 2023
7ee951d
now with task cancellation
semorrison Nov 19, 2023
54c5b79
another test of the cancellation hook
semorrison Nov 19, 2023
1689861
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Nov 19, 2023
a7d6308
.
semorrison Nov 19, 2023
d6edaf7
update TODO
semorrison Nov 19, 2023
335a414
slightly refactor
semorrison Nov 19, 2023
4866672
missing doc-string
semorrison Nov 19, 2023
4cbc8bb
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Nov 19, 2023
b657004
chore: bump Std
semorrison Nov 19, 2023
28f88a2
remove upstreamed lemmas
semorrison Nov 19, 2023
f6c8115
.
semorrison Nov 20, 2023
02ab251
Merge branch 'bump_std18' into hint_tactic_parallel
semorrison Nov 20, 2023
f5d9e00
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Nov 20, 2023
01a40a6
Apply suggestions from code review
semorrison Nov 26, 2023
6f1826b
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Nov 26, 2023
831a3de
use high priority tasks for map steps
semorrison Nov 27, 2023
e0e99b4
chore: reduce imports of Data.List.Defs
semorrison Nov 27, 2023
8213334
import all
semorrison Nov 27, 2023
707e2fa
Merge branch 'data_list_defs' into hint_tactic_parallel
semorrison Nov 27, 2023
f59153b
fix test
semorrison Nov 28, 2023
aeba8d0
Merge branch 'data_list_defs' into hint_tactic_parallel
semorrison Nov 28, 2023
d9b197e
Update Mathlib/Data/List/BigOperators/Defs.lean
semorrison Nov 29, 2023
a0e9e06
fix test (congr.lean)
thorimur Nov 30, 2023
b970465
fix `rel_prod` type signature
thorimur Nov 30, 2023
2d708e0
remove autoImplicit
semorrison Dec 10, 2023
25c3b1b
Merge branch 'data_list_defs' into hint_tactic_parallel
semorrison Dec 10, 2023
b98ab8b
Merge remote-tracking branch 'origin/master' into hint_tactic_parallel
semorrison Dec 12, 2023
c78a96f
fix
semorrison Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Mathlib.lean
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,7 @@ import Mathlib.Data.MLList.BestFirst
import Mathlib.Data.MLList.Dedup
import Mathlib.Data.MLList.DepthFirst
import Mathlib.Data.MLList.IO
import Mathlib.Data.MLList.Meta
import Mathlib.Data.MLList.Split
import Mathlib.Data.Matrix.Auto
import Mathlib.Data.Matrix.Basic
Expand Down
224 changes: 224 additions & 0 deletions Mathlib/Data/MLList/Meta.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/-
Copyright (c) 2023 Scott Morrison. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Scott Morrison
-/
import Mathlib.Data.MLList.IO
import Mathlib.Data.List.Defs

/-!
# Parallelization in Lean's tactic monads.

`MetaM.runGreedily` will run a list of `MetaM α` in parallel, returning
* a cancellation hook and
* an `MLList MetaM α` that returns the results (and the relevant `MetaM` state)
in the order that they finish.

After calling the cancellation hook the behaviour of the monadic lazy list should not be relied on.
It may continue returning values, or fail.
Recommended usage is to take a prefix of the list
(e.g. with `MLList.takeUpToFirst` followed by `MLList.force`, or `MLList.takeAsList`)
and then call the cancellation hook to request cancellation of later unwanted tasks.

(Similarly also for `CoreM`, `TermElabM`, and `TacticM`.)

## Implementation notes:
Calling `IO.cancel` on `t.map f` does not cancel `t`,
so we have to be careful throughout this file
to construct cancellation hooks connected to the underlying task,
rather than the various maps of it that we construct to pass state.

Thomas Murrills has a suggestion to significantly refactor this code,
reducing duplication using `MonadControl`, but it will require a core change.
See https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/.60StateRefT'.60.20.60MonadControl.60.20instance.3F
-/

set_option autoImplicit true

namespace IO

/--
Given a list of values in `IO α`, executes them all in parallel as tasks, and returns
* a cancellation hook and
* a monadic lazy list which returns the values in the order they complete.

Note that the cancellation hook merely requests cooperative cancellation:
the tasks must call `IO.checkCanceled` themselves.

After calling the cancellation hook the behaviour of the monadic lazy list should not be relied on.
It may continue returning values, or fail.
Recommended usage is to take a prefix of the list
(e.g. with `MLList.takeUpToFirst` followed by `MLList.force`, or `MLList.takeAsList`)
and then call the cancellation hook to request cancellation of later unwanted tasks.
-/
def runGreedily (tasks : List (IO α)) : IO (BaseIO Unit × MLList IO α) := do
let t ← show BaseIO _ from (tasks.map IO.asTask).traverse id
return (t.forM cancel, MLList.ofTaskList t |>.liftM |>.mapM fun
| .ok a => pure a
| .error e => throw (IO.userError s!"{e}"))

/--
Variant of `IO.runGreedily` without a cancellation hook.
-/
def runGreedily' (tasks : List (IO α)) : MLList IO α :=
.squash fun _ => (·.2) <$> runGreedily tasks

end IO

namespace Lean.Core.CoreM

/--
Given a monadic value in `CoreM`, creates a task that runs it in the current state,
returning
* a cancellation hook and
* a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask (t : CoreM α) : CoreM (BaseIO Unit × Task (CoreM α)) := do
let task ← (t.toIO (← read) (← get)).asTask
return (IO.cancel task, task.map (prio := .max) fun
| .ok (a, s) => do set s; pure a
| .error e => throwError m!"Task failed:\n{e}")

/--
Given a monadic value in `CoreM`, creates a task that runs it in the current state,
returning a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask' (t : CoreM α) : CoreM (Task (CoreM α)) := (·.2) <$> asTask t

/--
Given a list of monadic values in `CoreM`, runs them all as tasks,
and returns
* a cancellation hook and
* the monadic lazy list which returns the values in the order they complete.

See the doc-string for `IO.runGreedily` for details about the cancellation hook behaviour.
-/
def runGreedily (jobs : List (CoreM α)) : CoreM (BaseIO Unit × MLList CoreM α) := do
let (cancels, tasks) := (← jobs.mapM asTask).unzip
return (cancels.forM id, .squash fun _ => return MLList.ofTaskList tasks |>.liftM.mapM id)

/--
Variant of `CoreM.runGreedily` without a cancellation hook.
-/
def runGreedily' (jobs : List (CoreM α)) : MLList CoreM α :=
.squash fun _ => (·.2) <$> runGreedily jobs

end Lean.Core.CoreM

namespace Lean.Meta.MetaM

/--
Given a monadic value in `MetaM`, creates a task that runs it in the current state,
returning
* a cancellation hook and
* a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask (t : MetaM α) : MetaM (BaseIO Unit × Task (MetaM α)) := do
let (cancel, task) ← (t.run (← read) (← get)).asTask
return (cancel, task.map (prio := .max)
fun c : CoreM (α × Meta.State) => do let (a, s) ← c; set s; pure a)

/--
Given a monadic value in `MetaM`, creates a task that runs it in the current state,
returning a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask' (t : MetaM α) : MetaM (Task (MetaM α)) := (·.2) <$> asTask t

/--
Given a list of monadic values in `MetaM`, runs them all as tasks,
and returns
* a cancellation hook and
* the monadic lazy list which returns the values in the order they complete.

See the doc-string for `IO.runGreedily` for details about the cancellation hook behaviour.
-/
def runGreedily (jobs : List (MetaM α)) : MetaM (BaseIO Unit × MLList MetaM α) := do
let (cancels, tasks) := (← jobs.mapM asTask).unzip
return (cancels.forM id, .squash fun _ => return MLList.ofTaskList tasks |>.liftM.mapM id)

/--
Variant of `MetaM.runGreedily` without a cancellation hook.
-/
def runGreedily' (jobs : List (MetaM α)) : MLList MetaM α :=
.squash fun _ => (·.2) <$> runGreedily jobs


end Lean.Meta.MetaM

namespace Lean.Elab.Term.TermElabM

/--
Given a monadic value in `TermElabM`, creates a task that runs it in the current state,
returning
* a cancellation hook and
* a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask (t : TermElabM α) : TermElabM (BaseIO Unit × Task (TermElabM α)) := do
let (cancel, task) ← (t.run (← read) (← get)).asTask
return (cancel, task.map (prio := .max)
fun c : MetaM (α × Term.State) => do let (a, s) ← c; set s; pure a)

/--
Given a monadic value in `TermElabM`, creates a task that runs it in the current state,
returning a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask' (t : TermElabM α) : TermElabM (Task (TermElabM α)) := (·.2) <$> asTask t

/--
Given a list of monadic values in `TermElabM`, runs them all as tasks,
and returns
* a cancellation hook and
* the monadic lazy list which returns the values in the order they complete.

See the doc-string for `IO.runGreedily` for details about the cancellation hook behaviour.
-/
def runGreedily (jobs : List (TermElabM α)) : TermElabM (BaseIO Unit × MLList TermElabM α) := do
let (cancels, tasks) := (← jobs.mapM asTask).unzip
return (cancels.forM id, .squash fun _ => return MLList.ofTaskList tasks |>.liftM.mapM id)

/--
Variant of `TermElabM.runGreedily` without a cancellation hook.
-/
def runGreedily' (jobs : List (TermElabM α)) : MLList TermElabM α :=
.squash fun _ => (·.2) <$> runGreedily jobs

end Lean.Elab.Term.TermElabM

namespace Lean.Elab.Tactic.TacticM

/--
Given a monadic value in `TacticM`, creates a task that runs it in the current state,
returning
* a cancellation hook and
* a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask (t : TacticM α) : TacticM (BaseIO Unit × Task (TacticM α)) := do
let (cancel, task) ← (t (← read) |>.run (← get)).asTask
return (cancel, task.map (prio := .max)
fun c : TermElabM (α × Tactic.State) => do let (a, s) ← c; set s; pure a)

/--
Given a monadic value in `TacticM`, creates a task that runs it in the current state,
returning a monadic value with the cached result (and subsequent state as it was after running).
-/
def asTask' (t : TacticM α) : TacticM (Task (TacticM α)) := (·.2) <$> asTask t

/--
Given a list of monadic values in `TacticM`, runs them all as tasks,
and returns
* a cancellation hook and
* the monadic lazy list which returns the values in the order they complete.

See the doc-string for `IO.runGreedily` for details about the cancellation hook behaviour.
-/
def runGreedily (jobs : List (TacticM α)) : TacticM (BaseIO Unit × MLList TacticM α) := do
let (cancels, tasks) := (← jobs.mapM asTask).unzip
return (cancels.forM id, .squash fun _ => return MLList.ofTaskList tasks |>.liftM.mapM id)

/--
Variant of `TacticM.runGreedily` without a cancellation hook.
-/
def runGreedily' (jobs : List (TacticM α)) : MLList TacticM α :=
.squash fun _ => (·.2) <$> runGreedily jobs

end Lean.Elab.Tactic.TacticM
50 changes: 29 additions & 21 deletions Mathlib/Tactic/Hint.lean
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ Authors: Scott Morrison
-/
import Std.Tactic.TryThis
import Std.Linter.UnreachableTactic
import Mathlib.Data.Nondet.Basic
import Mathlib.Tactic.FailIfNoProgress
import Mathlib.Mathport.Rename
import Mathlib.Data.MLList.Meta

/-!
# The `hint` tactic.
Expand All @@ -16,8 +14,6 @@ The `hint` tactic tries the kitchen sink:
it runs every tactic registered via the `register_hint tac` command
on the current goal, and reports which ones succeed.

## Future work
It would be nice to run the tactics in parallel.
-/

open Lean Elab Tactic
Expand Down Expand Up @@ -93,28 +89,40 @@ def withoutInfoTrees (t : TacticM Unit) : TacticM Unit := do
modifyInfoState fun s => { s with trees }

/--
Run all tactics registered using `register_hint`.
Print a "Try these:" suggestion for each of the successful tactics.
Run all tactics registered using `register_hint`,
returning a list of triples consisting of
* a `Suggestion` (for "Try this"),
* the `List MVarId` of remaining goals, and
* the `Tactic.SavedState` after running the tactic.

If one tactic succeeds and closes the goal, we don't look at subsequent tactics.
If one tactic succeeds and closes the goal, we cancel subsequent tactics.
-/
-- TODO We could run the tactics in parallel.
-- TODO With widget support, could we run the tactics in parallel
-- and do live updates of the widget as results come in?
def hint (stx : Syntax) : TacticM Unit := do
let tacs := Nondet.ofList (← getHints)
let results := tacs.filterMapM fun t : TSyntax `tactic => do
def hintCore : TacticM (Array (Suggestion × List MVarId × SavedState)) := do
let tacs ← getHints
let tasks := tacs.map fun t : TSyntax `tactic => do
if let some msgs ← observing? (withMessageLog (withoutInfoTrees (evalTactic t))) then
return some (← getGoals, ← suggestion t msgs)
return some (← suggestion t msgs, ← getGoals, ← saveState)
else
return none
let results ← (results.toMLList.takeUpToFirst fun r => r.1.1.isEmpty).asArray
let results := results.qsort (·.1.1.length < ·.1.1.length)
addSuggestions stx (results.map (·.1.2))
match results.find? (·.1.1.isEmpty) with
| some r =>
let (cancel, results) ← TacticM.runGreedily tasks
let results := results.filterMap id
let results ← (results.takeUpToFirst fun (_, mvars, _) => mvars.isEmpty).asArray
cancel -- Cancel any remaining tasks, in case one closed the goal early.
return results.qsort fun (_, mvars₁, _) (_, mvars₂, _) => mvars₁.length < mvars₂.length

/--
Run all tactics registered using `register_hint`.
Print a "Try these:" suggestion for each of the successful tactics.

If one tactic succeeds and closes the goal, we cancel subsequent tactics.
-/
def hint (stx : Syntax) : TacticM Unit := do
let results ← hintCore
addSuggestions stx (results.map (·.1))
match results.find? fun (_, mvars, _) => mvars.isEmpty with
| some (_, _, s) =>
-- We don't restore the entire state, as that would delete the suggestion messages.
setMCtx r.2.term.meta.meta.mctx
setMCtx s.term.meta.meta.mctx
| none => admitGoal (← getMainGoal)

/--
Expand Down
59 changes: 59 additions & 0 deletions test/MLList.lean
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Authors: Scott Morrison
import Std.Data.MLList.Basic
import Mathlib.Control.Basic
import Mathlib.Tactic.RunCmd
import Std.Tactic.GuardMsgs
import Mathlib.Data.MLList.Meta

@[reducible] def S (α : Type) := StateT (List Nat) Option α
def append (x : Nat) : S Unit :=
Expand Down Expand Up @@ -67,3 +69,60 @@ run_cmd Lean.Elab.Command.liftTermElabM do
guard (n = 5)
pure n
guard $ n = 5

/-!
Tests for `runGreedily`, which converts a `List (MetaM α)` into a `MLList MetaM α`,
streaming results as they become available.
-/

def busy_wait (millis : Nat) : IO Unit := do
let start ← IO.monoMsNow
while !(← IO.checkCanceled) && (← IO.monoMsNow) < start + millis do pure ()
if (← IO.checkCanceled) then throw <| IO.userError "cancelled"

open Lean.Meta.MetaM

/--
info: 0
-/
#guard_msgs in
#eval show MetaM _ from do
-- We put an `IO.sleep 0` in the long calculation to prevent Lean from optimizing away the `do`.
let t : List (MetaM Nat) := [do busy_wait 1000; pure 1000, do busy_wait 0; pure 0]
let r := runGreedily' t
r.head

/--
info: 1
-/
#guard_msgs in
#eval show MetaM _ from do
let t : List (MetaM Nat) := [do busy_wait 0; pure 1, do busy_wait 5; pure 0]
let r := runGreedily' t
r.head

/--
info: Without cancellation:
Result: 0
Results after waiting: [10, 0]
With cancellation:
Result: 0
Results after waiting: [0]
-/
#guard_msgs in
#eval show MetaM _ from do
let ref : IO.Ref (List Nat) ← IO.mkRef []
let store : Nat → IO Nat := fun n => do ref.modify fun l => n :: l; pure n
let t : List (MetaM Nat) := [do busy_wait 10; store 10, do busy_wait 0; store 0]
let r := runGreedily' t
IO.println "Without cancellation:"
IO.println s!"Result: {← r.head}"
IO.sleep 20
IO.println s!"Results after waiting: {← ref.get}"
IO.println "With cancellation:"
ref.set []
let (c, r) ← runGreedily t
IO.println s!"Result: {← r.head}"
_ ← c
IO.sleep 20
IO.println s!"Results after waiting: {← ref.get}"
Loading
Loading