Zulip Chat Archive
Stream: new members
Topic: Lean 4 Tasks
Sébastien Boisgérault (Jan 14 2026 at 19:13):
Hi everyone! :wave:
I was (naively?) expecting the following code to implement a parallel map:
def pmap {α β} (f : α → β) (inputs : List α) : List β :=
let t_inputs : List (Task α) := inputs.map Task.pure
let t_outputs := t_inputs.map (fun t_input => t_input.map f)
t_outputs.map Task.get
(since my understanding was that the calls to fun t_input => t_input.map fhappen sequentially but each call is non-blocking and starts the application of f in the background).
But my experiments seem to show that a single thread is used anyway, for example when I run
def countdown (n : Nat) : Nat :=
match n with
| 0 => 0
| n + 1 => countdown n
def test : IO Unit :=
let n := 10_000_000_000
let inputs := [n, 2 * n, 3 * n, 4 * n, 5 * n, 6 * n, 7 * n, 8 * n]
inputs |> pmap countdown |> IO.println
def main := test
Can you pinpoint where I am wrong?
Kind regards,
Sébastien
Mirek Olšák (Jan 14 2026 at 20:02):
Interesting, when I run it with metaprogramming, it paralelizes. By the way it also paralellizes with the countdown but run at your own risk.
def test : Lean.Meta.MetaM (List Nat) :=
let n := 10_000_000_000
let inputs := [n, 2 * n, 3 * n, 4 * n, 5 * n, 6 * n, 7 * n, 8 * n]
return pmap (fun x ↦ dbgSleep 1000 (fun _ ↦ x)) inputs
-- return pmap countdown inputs
run_meta test
Robin Arnez (Jan 14 2026 at 20:08):
The code of lean_task_map_core:
extern "C" LEAN_EXPORT obj_res lean_task_map_core(obj_arg f, obj_arg t, unsigned prio,
bool sync, bool keep_alive) {
if (!g_task_manager || (sync && lean_to_task(t)->m_value)) {
return lean_task_pure(apply_1(f, lean_task_get_own(t)));
} else {
lean_task_object * new_task = alloc_task(mk_closure_3_2(task_map_fn, f, t), sync ? LEAN_SYNC_PRIO : prio, keep_alive);
g_task_manager->add_dep(lean_to_task(t), new_task);
return (lean_object*)new_task;
}
}
I suppose if no task has ever been spawned then g_task_manager is null and it never considers to spawn a task. I think the reason this is happening is that it is expected that the task t was spawned using e.g. docs#Task.spawn and then docs#Task.map would register that f should run after the original task finished on the same thread as the original task.
That is, the intended way to use the system would rather be:
def pmap {α β} (f : α → β) (inputs : List α) : List β :=
let t_outputs := inputs.map (fun input => Task.spawn fun _ => f input)
t_outputs.map Task.get
or maybe
def pmap {α β} (f : α → β) (inputs : List α) : List β :=
let t_inputs := inputs.map (fun input => Task.spawn fun _ => input)
let t_outputs := t_inputs.map (fun t_input => t_input.map f)
t_outputs.map Task.get
Sébastien Boisgérault (Jan 14 2026 at 22:37):
OK, I can understand the issue with not spawning explicitly any task. But if I actually also tried a pmap that corresponds closely to your second solution:
def pmap {α β} (f : α → β) (inputs : List α) : List β :=
-- let t_inputs : List (Task α) := inputs.map Task.pure
let t_inputs : List (Task α) := inputs.map fun input => Task.spawn (fun () => input)
let t_outputs := t_inputs.map (fun t_input => t_input.map f)
t_outputs.map Task.get
and AFAICT, I still get only 100% for the process when I run top (which I interpret as "not running in parallel", but maybe I am wrong?).
On the other hand, I get initially a CPU load with top of ~ 800% when I run for example
def main : IO Unit :=
let n := 10_000_000_000
let inputs := [n, 2 * n, 3 * n, 4 * n, 5 * n, 6 * n, 7 * n, 8 * n]
let tasks := inputs.map fun input => (Task.spawn fun () => countdown input)
let outputs := tasks.map Task.get
IO.println outputs
Is there something else going on here?
Sébastien Boisgérault (Jan 14 2026 at 22:50):
Update. Well, I need to reinvestigate all this, everything is "only" à 100% CPU now including the baseline that was using 800% CPU before. I need to examine the options of top (or htop) more closely.
Sébastien Boisgérault (Jan 14 2026 at 23:11):
Update. I may have messed up something with the Lake project itself, the program doesn't seem to be refreshed when I edit it and redo a lake build. I will create a smaller, simpler project with only these Tasks-related test and will report back here.
Thanks everyone for your help so far! :pray:
Sébastien Boisgérault (Jan 15 2026 at 10:01):
OK, I am officially confused. Running the code
def countdown (n : Nat) : Nat :=
match n with
| 0 => 0
| n + 1 => countdown n
def main : IO Unit := do
let n := 10_000_000_000
let inputs := 8 |> List.range |>.map (· + 1) |>.map (· * n)
IO.println s!"countdown inputs: {inputs}"
let t0 <- IO.monoMsNow
let tasks := inputs.map fun input => (Task.spawn fun () => countdown input)
let outputs := tasks.map Task.get
let dt := (<- IO.monoMsNow) - t0
IO.println s!"countdown outputs: {outputs}"
IO.println s!"elapsed time: {dt} ms"
takes some time, then yields
countdown inputs: [10000000000, 20000000000, 30000000000, 40000000000, 50000000000, 60000000000, 70000000000, 80000000000]
countdown outputs: [0, 0, 0, 0, 0, 0, 0, 0]
elapsed time: 0 ms
List.map is not lazy here, right? And Task.get should be blockin, so why is the elapsed time 0?
Update. Ah, I guess that Lean probably feels ok with reordering the task code since it is pure? OK, let's for get about this side issue for a moment, I can put the time measure after the final IO.println.
Sébastien Boisgérault (Jan 15 2026 at 11:03):
Final update. OK, sorry for the noise everyone, I was wrong.
After carefully doing all the experiments again, tasks are parallelized in all cases, including when I am using Task.pure (when Task.spawn could be more "natural").
That means that
def pmap {α β} (f : α → β) (inputs : List α) : List β :=
let t_inputs := inputs.map Task.pure
let t_outputs := t_inputs.map (fun t_input => t_input.map f)
t_outputs.map Task.get
works in parallel and that I can rewrite it as:
import Lake -- declare Task as a Monad
def pmap {α β} (f : α → β) (inputs : List α) : List β :=
inputs
|>.map (fun input : α => return input)
|>.map (fun t_input => return f (<- t_input))
|>.map Task.get
if I wish.
Sébastien Boisgérault (Jan 15 2026 at 11:05):
Git repo: https://github.com/boisgera/lean-tasks
Last updated: Feb 28 2026 at 14:05 UTC