Zulip Chat Archive

Stream: general

Topic: Concurrent lean


Mario Carneiro (Mar 28 2018 at 01:54):

I've been thinking about what appropriate concurrency primitives can be used in pure lean. @Gabriel Ebner Do you see any problems with modeling task in lean (being the same as thunk)? It's not clear why this needs to be meta.

Another possible concurrency primitive is fork : thunk A -> thunk B -> A x B which runs the two thunks on separate threads and then returns the results. There is an obvious generalization here to peval : list (thunk A) -> list A, not sure if this is better or worse than join as a primitive. I'm not sure how to simulate these forking strategies with task.

Mario Carneiro (Mar 28 2018 at 01:56):

I hoped to get inspiration from ML and Haskell, but ML is too declarative (unit functions everywhere) and Haskell has funny problems with laziness turning the evaluation order inside out. https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/parallel_haskell2.pdf

Sebastian Ullrich (Mar 28 2018 at 07:41):

fork f g = let tf = task.delay t in let tg = task.delay g in (tf.get, tg.get)?

Gabriel Ebner (Mar 28 2018 at 07:43):

task is perfectly sound, and does not need to be meta. A possible model for task is id, and you should not be able to observe any differences from id in pure Lean code.

Sebastian Ullrich (Mar 28 2018 at 07:44):

Or less resource-hungry, fork f g = let tf = task.delay f in let y = g () in (tf.get, y)

Mario Carneiro (Mar 28 2018 at 07:47):

Isn't the evaluation semantics off from id though? You need thunk to avoid evaluating the argument too soon

Gabriel Ebner (Mar 28 2018 at 07:47):

That's why I said pure Lean code. :smile:

Gabriel Ebner (Mar 28 2018 at 07:48):

Of course, task hides nontermination and C++ exceptions.

Sebastian Ullrich (Mar 28 2018 at 07:48):

One missing ingredient right now is task.delay_io : {α : Type u} (f : io α) : io (task α)

Mario Carneiro (Mar 28 2018 at 07:48):

Neither can happen in non-meta code though, right?

Mario Carneiro (Mar 28 2018 at 07:48):

How is that different from task.delay?

Gabriel Ebner (Mar 28 2018 at 07:49):

You can have nontermination without meta if you add axiom large_cardinal : 0 = 1

Mario Carneiro (Mar 28 2018 at 07:49):

lol that's quite a large cardinal

Sebastian Ullrich (Mar 28 2018 at 07:51):

Okay, I think I fixed the signature

Gabriel Ebner (Mar 28 2018 at 07:51):

Right now, there is also the slight performance problem that we serialize the thunk passed to task.delay, and then deserialize it on the thread where it executes. From the last public state of the Lean 4 design document, this should be solved in Lean 4.

Mario Carneiro (Mar 28 2018 at 07:51):

Well, you can break the VM in many ways using large_cardinal, you don't need task

Mario Carneiro (Mar 28 2018 at 07:52):

in fact you should be able to get nontermination even without task

Gabriel Ebner (Mar 28 2018 at 07:52):

in fact you should be able to get nontermination even without task

New results from your master thesis?

Mario Carneiro (Mar 28 2018 at 07:53):

No, maybe it's folklore... But you can just evaluate a false proof of well_founded in the VM

Gabriel Ebner (Mar 28 2018 at 07:54):

Ok, that's essentially the same thing as large_cardinal, or sorry.

Mario Carneiro (Mar 28 2018 at 07:54):

right, that's my point - from a false axiom you can fake a wf definition to cause nontermination

Mario Carneiro (Mar 28 2018 at 07:55):

(in the VM, which doesn't do reduction and so doesn't get stuck like #reduce would)

Mario Carneiro (Mar 28 2018 at 07:56):

What is the behavior of let f := task.delay f in ()?

Mario Carneiro (Mar 28 2018 at 07:57):

Does it just drop the thread?

Mario Carneiro (Mar 28 2018 at 07:57):

What if you call task.get twice?

Gabriel Ebner (Mar 28 2018 at 07:58):

What is the behavior of let f := task.delay f in ()?

f continues to execute, you can continue printing trace messages, etc., and they will show up.

Gabriel Ebner (Mar 28 2018 at 07:59):

What if you call task.get twice?

The first time you call task.get, it will wait for the task to finish and return the result. The second time it will return immediately with the result. The code in the task is only executed once.

Mario Carneiro (Mar 28 2018 at 07:59):

Ooh, that sounds like memoization

Mario Carneiro (Mar 28 2018 at 08:00):

Maybe memoized is definable after all...

Sebastian Ullrich (Mar 28 2018 at 08:00):

It's what thunk should sound like :laughing:

Mario Carneiro (Mar 28 2018 at 08:01):

I think Leo said something about explicitly wanting thunk to not be memoized, although I think it's a poor name if that's the intent

Gabriel Ebner (Mar 28 2018 at 08:01):

It's like memoization except that the code starts running immediately, and on another thread. C++ code has the ability to create tasks that do not start immediately though (in fact, that's even the default behavior).

Sebastian Ullrich (Mar 28 2018 at 08:01):

Btw, we will most probably move those horrible impure init.util functions into io

Mario Carneiro (Mar 28 2018 at 08:02):

You can create delayed tasks with thunk (task A) I guess

Gabriel Ebner (Mar 28 2018 at 08:02):

This will create a new task every time you evaluate the thunk, probably not what you want.

Mario Carneiro (Mar 28 2018 at 08:03):

hm... task (task A)? :)

Gabriel Ebner (Mar 28 2018 at 08:03):

Immediately creates a task on another thread...

Mario Carneiro (Mar 28 2018 at 08:04):

but I guess if the real work is in the created task that won't be executed until you flatten

Gabriel Ebner (Mar 28 2018 at 08:05):

If you do task.delay (task.delay ()), you first create task A, which immediately begins executing on thread 2. This task A then creates task B, which again immediately begins executing () on thread 3.

Mario Carneiro (Mar 28 2018 at 08:06):

Oh, I see...

Mario Carneiro (Mar 28 2018 at 08:08):

@Sebastian Ullrich I really like those non-io functions. As long as they are observationally pure there is no problem

Gabriel Ebner (Mar 28 2018 at 08:08):

Do you actually use them?

Sebastian Ullrich (Mar 28 2018 at 08:08):

:)

Mario Carneiro (Mar 28 2018 at 08:08):

I use timeit and trace all the time

Sebastian Ullrich (Mar 28 2018 at 08:09):

Hmm, good to know I guess

Mario Carneiro (Mar 28 2018 at 08:10):

Actually I've never used io in pure code. It seems like a trap

Gabriel Ebner (Mar 28 2018 at 08:10):

I just realized that you can't even simulate them via unsafe_perform_io, since that is meta.

Mario Carneiro (Mar 28 2018 at 08:12):

Here's another missing task primitive: Run f g : thunk A, and return the first one that finishes

Gabriel Ebner (Mar 28 2018 at 08:12):

That's nondeterministic and hence unsupported by design.

Mario Carneiro (Mar 28 2018 at 08:12):

The obvious problem is that they might not return the same thing, but cached solves that problem handily

Mario Carneiro (Mar 28 2018 at 08:13):

I think it should probably be renamed nondet

Sebastian Ullrich (Mar 28 2018 at 08:13):

Did you see my beautiful typeclass for lifting these functions in the extended monad PR? Fortunately I found a more general monad class that subsumes it https://github.com/Kha/lean/commit/974fee78f75c8861477fba9143a49c8e08101b41#diff-f8e23e5e864df768277396e6f07b2809L67

Gabriel Ebner (Mar 28 2018 at 08:14):

@Sebastian Ullrich This does not help you if you want to do printf-style debugging in a pure non-monadic function, right?

Sebastian Ullrich (Mar 28 2018 at 08:16):

Then you don't need any lifting, no? This is still with the current pure definition instead of in io.

Mario Carneiro (Mar 28 2018 at 08:17):

Is there a @[noinline] or @[volatile] attribute for these functions?

Gabriel Ebner (Mar 28 2018 at 08:17):

Right. You proposed the lifting functions as a replacement for the pure ones, that's what I was responding to.

Mario Carneiro (Mar 28 2018 at 08:20):

Alternatively, you could accept a proof f () = g () to eliminate the nondeterminism. This is roughly equivalent to the cached approach without boxing up the result

Mario Carneiro (Mar 28 2018 at 08:21):

(This is not an unreasonable constraint, if it's not true you can just quotient the output type to make it true)

Sebastian Ullrich (Mar 28 2018 at 08:23):

@Gabriel Ebner No, that was intended for correctly lifting "pure" scoping functions thunk A -> A into thunk (state S A) -> state S A, etc. But the same approach would be used for lifting io A -> io A scoping functions, yes. The good thing is that with the latter, you can't accidentally forget the lift. Which is the main reason why I think the current definitions are pretty evil.

Gabriel Ebner (Mar 28 2018 at 08:24):

Ah, I see.

Mario Carneiro (Mar 28 2018 at 08:25):

I think that using scoping functions to build an io computation is certainly bad practice; the trace output should be expected to be nondeterministic

Mario Carneiro (Mar 28 2018 at 08:26):

what is unrun? That seems a bit paradoxical

Sebastian Ullrich (Mar 28 2018 at 08:27):

unrun has since been removed :)

Mario Carneiro (Mar 28 2018 at 08:30):

Now that io_interface is gone, is it impossible to non-meta write io programs?

Sebastian Ullrich (Mar 28 2018 at 08:30):

io is non-meta now :)

Mario Carneiro (Mar 28 2018 at 08:31):

yeah, but not really, it relies on a meta constant that is not actually meta for some reason

Sebastian Ullrich (Mar 28 2018 at 08:34):

I think the idea was to not force users to annotate all io functions with meta. You can't prove anything about it, but it's still sound. The replacement for io_interface is monad_io.

Mario Carneiro (Mar 28 2018 at 08:34):

Oh, I guess the idea is you should replace io_core with your own implementation of monad_io_terminal etc

Mario Carneiro (Mar 28 2018 at 08:37):

How do you know it's sound? Writing an implementation is not trivial, I think

Mario Carneiro (Mar 28 2018 at 08:38):

in particular there is a constructor that gives unbounded recursion in the monad

Sebastian Ullrich (Mar 28 2018 at 08:39):

Which should be okay as long as there's no eliminator, no?

Mario Carneiro (Mar 28 2018 at 08:42):

hm, I guess you can do some kind of coinductive trace thing if you don't want to have it be trivial

Mario Carneiro (Mar 28 2018 at 08:43):

Oh cool I just noticed we have a random number generator in lean now

Gabriel Ebner (Mar 28 2018 at 08:44):

For soundness it should be enough to set io_core := λ _ _, unit.

Mario Carneiro (Mar 28 2018 at 08:45):

is unit a monad in lean?

Sebastian Ullrich (Mar 28 2018 at 08:46):

It's not even a type constructor...?

Mario Carneiro (Mar 28 2018 at 08:46):

or I guess λ _, unit

Sebastian Ullrich (Mar 28 2018 at 08:47):

There's also https://github.com/leanprover/lean/blob/efa9d7e110ff6389e8863551a4882c84dbcf8236/tests/lean/run/rebind_bind.lean

Mario Carneiro (Mar 28 2018 at 08:48):

interesting. You are using do notation with something that's not monad.bind?

Sebastian Ullrich (Mar 28 2018 at 08:49):

Yep, that's the main purpose of the test

Kevin Buzzard (Mar 28 2018 at 10:20):

mono_monad? Reminds me of https://www.youtube.com/watch?v=8N_tupPBtWQ

Mario Carneiro (Mar 30 2018 at 07:05):

Check it out:

meta def list.par_map {α β} (f : α → β) (l : list α) : list β :=
let fs := l.map (λ a, task.delay (λ _, f a)) in
fs.map task.get

#eval timeit "" ((list.range' 50000 10).map (λ n, (eratosthenes n).length))
-- 6.87s
#eval timeit "" ((list.range' 50000 10).par_map (λ n, (eratosthenes n).length))
-- 2.78s

Mario Carneiro (Mar 30 2018 at 07:06):

too bad it has to be meta

Adam Kurkiewicz (Mar 30 2018 at 12:51):

impressive! What keeps it in meta?


Last updated: Dec 20 2023 at 11:08 UTC