-
Notifications
You must be signed in to change notification settings - Fork 30
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
sketch proposal / brainstorming for the Domainslib API #92
Comments
I think this is moving in a brighter direction. Question: it seems impossible for |
I'm not sure what your distinction is between anonymous and non-anonymous worksets, but I realized that I forgot to expose a I think it is possible to mix distinct worksets in the same computation by nesting |
Got it. (By "anonymous" I meant "worksets created internally by |
Is the distinction between Relatedly, what would be the semantics of |
My intuition is that separating the two concepts makes them both simpler, easier to specify and reason about. Currently it is weird that a task runner may steal jobs from a domain pool when it is blocked waiting on something else, and I think it is the root cause of the confusion that is discussed in #77. With the separated design, worksets are just a concurrent data-structure storing jobs, while pools are just worker domains that repeatedly service jobs from a workset -- plus the ability to tear them down externally, which cannot be implemented with a In the previous API, everyone must know about pools, but worksets are a completely internal details of the pool implementation ... except they leak in the Task.run specification. With the proposed API, simple use-cases require knowledge of neither pools nor worksets,
This is a delicate part. Below I will explain my mental model (my best guesses as to what would be going on in your case), but it is likely to be wrong if we don't experiment with an actual prototype implementation. Mental model:
One advanced use-case involving I'm not sure whether |
Thinking about it, my own "advanced example" above is a bit silly, because instead of having independent worksets and a lock on |
Thinking about it again: I've grown convinced by @kayceesrk's point that separating pools and worksets is probably API over-engineering. Below is a simplified API where pools have an explicit function to steal a job. module Pool : sig
(** A pool of worker domains that are dedicated to performing jobs
received from a workset. It is only necessary to deal with Pool
for semi-advanced Task scheduling policies, for example if you
want to share the same domain pool between many short-running
tasks. *)
type t
val spawn : num_worker_domains:int -> pool
val get_num_worker_domains : t -> int
val steal_job_poll : pool -> job option
(** [steal_job_poll p] steals a job of [p] if available.
This function is non-blocking. *)
val teardown : t -> unit
end module Task : sig
(** A "task" abstraction for determining sub-computations to be
performed in parallel.
Tasks are effectful programs that use the effectful operations
provided by the Task module ([fork_join], [parallel_for],
[async], [await], etc. They must run in the dynamic scope of
a handler, one of the [run_*] functions. *)
type 'a task = unit -> 'a
(** Type of task *)
(** {2. Handlers} *)
val run : ?num_worker_domains:int -> 'a task -> 'a
(** [run t] runs the task [t] synchronously, using a temporary pool of worker domains
to offload asynchronous tasks in parallel.
By default the number of worker domains created is
[Domain.recommended_domain_count () - 1].
*)
val run_with_pool : Pool.t -> 'a task -> 'a
(** [run_with_pool p t] is an advanced version of [run t] that does
not spawn a temporary domain pool, but instead parks some
asynchronous tasks in the given pool [p].
During idle times (when waiting on an asynchronous task), the
calling domain will itself perform jobs from [p] -- note that
those jobs may be unrelated to the task, coming from other users
of the pool. *)
(** {2. Effectful operations -- must run in the dynamic scope of a handler.} *)
val fork_join : 'a task -> 'b task -> 'a * 'b
val fork_join_u : unit task -> unit task -> unit task
val parallel_for : ?chunk_size:int -> start:int -> finish:int ->
(int -> unit) -> unit
val parallel_for_reduce : ?chunk_size:int -> start:int -> finish:int ->
('a -> 'a -> 'a) -> 'a -> (int -> 'a) -> 'a
val parallel_scan : ('a -> 'a -> 'a) -> 'a array -> 'a array
val parallel_find : ?chunk_size:int -> start:int -> finish:int ->
(int -> 'a option) -> 'a option
type !'a promise
val async : 'a task -> 'a promise
val await : 'a promise -> 'a
end |
I like the new API. I have another idea---can we drop the
While we are on it, I wonder if it makes sense to also have a high-level interface on arrays?
|
The new API is better. More questions:
|
The distinction between
In my imagination,
In my imagination again, Task.run_with_pool handlers can be nested, so that sub-computations could in theory be sent to a different pool. In particular, if you have a specific crypto pool, you could define say If this idea of nesting handlers works (I would try it first before claiming that it does), I think it is nicer than making pools an optional argument of all task constructors, because it also gives the expressivity to conveniently run larger sub-tasks on a different pool.
If I was in charge of designing Domainslib (fortunately I'm not), I would not include named pools, which I think of as premature. It's very easy to do on the user side, and users are likely to do it better (safer) than what is currently proposed. On the other hand: I used to be very confident that hiding Pool creation under a |
Hello! I've spent some time in the past few weeks experimenting with trying to write highly optimized multicore OCaml code and thinking about some of these issues. In particular, I've experimented with various work-stealing and work-sharing implementations in my I believe that the realization that
mentioned by @gasche has a lot of merit. My work on the Hopac library, based on ideas from Cilk and Concurrent ML, for F# many years ago was based on that same fundamental idea. .NET and Windows provides several APIs for programming with threads, thread pools, etc. Back when I was working on Hopac I came to the conclusion (after many experiments) that it was not possible to implement super efficient schedulers on top of those existing APIs. That is why Hopac runs its own worker threads and schedules work on those worker threads. This provides very good performance when only Hopac threads are running. Unfortunately most existing applications and libraries use the existing .NET / Windows APIs or use their own worker threads, which then breaks the ideal one worker per thread model. I believe that we should try to come up with ways for asynchronous and parallel programming libraries to co-exist and co-operate profitably with minimal loss of performance so that applications do not have to unnecessarily choose between competing library ecosystems. For example, an application might use Eio (ping @talex5) for asynchronous programming and use some other library to run CPU intensive work in parallel. A single application might even use multiple different libraries for asynchronous and parallel programming. To address that goal I have put up a (work-in-progress) "proposal" for a library for the co-operative allocation of domains or the Please note that I have experimented with the |
In #77 we discussed the API of domainslib, and the fact that one could consider several changes, small or large. On my commute back today I tried to "brainstorm" on what another API for domainslib could look like -- same capabilities as today, but with a clearer separation of concern between the various pieces of the implementation. The result is included below for discussion.
The text was updated successfully, but these errors were encountered: