Skip to content

Commit 4713811

Browse files
kim-emclaude
andcommitted
perf: parallelize exact? and apply? tactics
Use `MetaM.parIterWithCancel` to try all candidate lemmas in parallel while preserving deterministic result ordering. When a complete solution is found, remaining tasks are cancelled and the result is returned immediately. This removes the old sequential `tryOnEach` implementation along with the heartbeat-based abortion mechanism (`abortSpeculation`, `mkHeartbeatCheck`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 272f0f5 commit 4713811

File tree

2 files changed

+34
-85
lines changed

2 files changed

+34
-85
lines changed

src/Lean/Elab/Tactic/LibrarySearch.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def exact? (ref : Syntax) (required : Option (Array (TSyntax `term))) (requireCl
3737
let allowFailure := fun g => do
3838
let g ← g.withContext (instantiateMVars (.mvar g))
3939
return required.all fun e => e.occurs g
40-
match (← librarySearch goal tactic allowFailure) with
40+
match ← librarySearch goal tactic allowFailure with
4141
-- Found goal that closed problem
4242
| none =>
4343
addExactSuggestion ref (← instantiateMVars (mkMVar mvar)).headBeta (checkState? := initialState)

src/Lean/Meta/Tactic/LibrarySearch.lean

Lines changed: 33 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ prelude
99
public import Lean.Meta.LazyDiscrTree
1010
public import Lean.Meta.Tactic.SolveByElim
1111
public import Lean.Util.Heartbeats
12+
public import Lean.Elab.Parallel
1213

1314
public section
1415

@@ -119,20 +120,6 @@ def libSearchFindDecls : Expr → MetaM (Array (Name × DeclMod)) :=
119120
(droppedKeys := droppedKeys)
120121
(constantsPerTask := constantsPerImportTask)
121122

122-
/--
123-
Return an action that returns true when the remaining heartbeats is less
124-
than the currently remaining heartbeats * leavePercent / 100.
125-
-/
126-
def mkHeartbeatCheck (leavePercent : Nat) : MetaM (MetaM Bool) := do
127-
let maxHB ← getMaxHeartbeats
128-
let hbThreshold := (← getRemainingHeartbeats) * leavePercent / 100
129-
-- Return true if we should stop
130-
pure $
131-
if maxHB = 0 then
132-
pure false
133-
else do
134-
return (← getRemainingHeartbeats) < hbThreshold
135-
136123
private def librarySearchEmoji : Except ε (Option α) → String
137124
| .error _ => bombEmoji
138125
| .ok (some _) => crossEmoji
@@ -157,24 +144,6 @@ def interleaveWith {α β γ} (f : α → γ) (x : Array α) (g : β → γ) (y
157144
(y.extract n y.size).map g
158145
pure $ res ++ last
159146

160-
/--
161-
An exception ID that indicates further speculation on candidate lemmas should stop
162-
and current results should be returned.
163-
-/
164-
private builtin_initialize abortSpeculationId : InternalExceptionId ←
165-
registerInternalExceptionId `Lean.Meta.LibrarySearch.abortSpeculation
166-
167-
/--
168-
Called to abort speculative execution in library search.
169-
-/
170-
def abortSpeculation [MonadExcept Exception m] : m α :=
171-
throw (Exception.internal abortSpeculationId {})
172-
173-
/-- Returns true if this is an abort speculation exception. -/
174-
def isAbortSpeculation : Exception → Bool
175-
| .internal id _ => id == abortSpeculationId
176-
| _ => false
177-
178147
section LibrarySearch
179148

180149
/--
@@ -241,59 +210,14 @@ private def librarySearchLemma (cfg : ApplyConfig) (act : List MVarId → MetaM
241210
else
242211
failure
243212

244-
/--
245-
Sequentially invokes a tactic `act` on each value in candidates on the current state.
246-
247-
The tactic `act` should return a list of meta-variables that still need to be resolved.
248-
If this list is empty, then no variables remain to be solved, and `tryOnEach` returns
249-
`none` with the environment set so each goal is resolved.
250-
251-
If the action throws an internal exception with the `abortSpeculationId` id then
252-
further computation is stopped and intermediate results returned. If any other
253-
exception is thrown, then it is silently discarded.
254-
-/
255-
def tryOnEach
256-
(act : Candidate → MetaM (List MVarId))
257-
(candidates : Array Candidate) :
258-
MetaM (Option (Array (List MVarId × MetavarContext))) := do
259-
let mut a := #[]
260-
let s ← saveState
261-
for c in candidates do
262-
match ← (tryCatch (Except.ok <$> act c) (pure ∘ Except.error)) with
263-
| .error e =>
264-
restoreState s
265-
if isAbortSpeculation e then
266-
break
267-
| .ok remaining =>
268-
if remaining.isEmpty then
269-
return none
270-
let ctx ← getMCtx
271-
restoreState s
272-
a := a.push (remaining, ctx)
273-
return (.some a)
274-
275-
private def librarySearch' (goal : MVarId)
276-
(tactic : List MVarId → MetaM (List MVarId))
277-
(allowFailure : MVarId → MetaM Bool)
278-
(leavePercentHeartbeats : Nat) :
279-
MetaM (Option (Array (List MVarId × MetavarContext))) := do
280-
withTraceNode `Tactic.librarySearch (return m!"{librarySearchEmoji ·} {← goal.getType}") do
281-
profileitM Exception "librarySearch" (← getOptions) do
282-
-- Create predicate that returns true when running low on heartbeats.
283-
let candidates ← librarySearchSymm libSearchFindDecls goal
284-
let cfg : ApplyConfig := { allowSynthFailures := true }
285-
let shouldAbort ← mkHeartbeatCheck leavePercentHeartbeats
286-
let act := fun cand => do
287-
if ←shouldAbort then
288-
abortSpeculation
289-
librarySearchLemma cfg tactic allowFailure cand
290-
tryOnEach act candidates
291-
292213
/--
293214
Tries to solve the goal either by:
294215
* calling `tactic true`
295216
* or applying a library lemma then calling `tactic false` on the resulting goals.
296217
218+
Runs all candidates in parallel, iterates through results in order.
219+
Cancels remaining tasks and returns immediately if a complete solution is found.
220+
297221
Typically here `tactic` is `solveByElim`,
298222
with the `Bool` flag indicating whether it may retry with `exfalso`.
299223
@@ -311,11 +235,36 @@ this is not currently tracked.)
311235
def librarySearch (goal : MVarId)
312236
(tactic : Bool → List MVarId → MetaM (List MVarId) :=
313237
fun initial g => solveByElim [] (maxDepth := 6) (exfalso := initial) g)
314-
(allowFailure : MVarId → MetaM Bool := fun _ => pure true)
315-
(leavePercentHeartbeats : Nat := 10) :
238+
(allowFailure : MVarId → MetaM Bool := fun _ => pure true) :
316239
MetaM (Option (Array (List MVarId × MetavarContext))) := do
317-
(tactic true [goal] *> pure none) <|>
318-
librarySearch' goal (tactic false) allowFailure leavePercentHeartbeats
240+
let tryDirect : MetaM (Option (Array (List MVarId × MetavarContext))) := do
241+
let _ ← tactic true [goal]
242+
pure none
243+
tryDirect <|> do
244+
withTraceNode `Tactic.librarySearch (return m!"{librarySearchEmoji ·} {← goal.getType}") do
245+
profileitM Exception "librarySearch" (← getOptions) do
246+
let candidates ← librarySearchSymm libSearchFindDecls goal
247+
let cfg : ApplyConfig := { allowSynthFailures := true }
248+
let jobs := candidates.toList.map fun cand => do
249+
let remaining ← librarySearchLemma cfg (tactic false) allowFailure cand
250+
pure (remaining, ← getMCtx)
251+
let (cancel, iter) ← MetaM.parIterWithCancelChunked jobs (maxTasks := 128)
252+
let mut partialSuccesses : Array (List MVarId × MetavarContext) := #[]
253+
for result in iter.allowNontermination do
254+
match result with
255+
| .ok (remaining, mctx) =>
256+
if remaining.isEmpty then
257+
-- Complete solution found - cancel remaining tasks and return
258+
cancel
259+
setMCtx mctx
260+
return none
261+
else
262+
-- Partial success - collect it
263+
partialSuccesses := partialSuccesses.push (remaining, mctx)
264+
| .error _ =>
265+
-- Task failed, continue to next
266+
continue
267+
return some partialSuccesses
319268

320269
end LibrarySearch
321270

0 commit comments

Comments
 (0)