multi-and many-core systems by exposing all available concurrency to a runtime system. Applications are expressed in terms of work-packages (tasks) with well-defined inputs and outputs, which guide the runtime scheduler in determining the correct execution order of tasks. A wide range of task-based programming models have been developed, ranging from node-local approaches such as OpenMP [40] and OmpSs [13] to distributed systems such as HPX [27] , StarPU [3] , DASH [49] , and PaRSEC [7] . All of them have in common that a set of tasks is scheduled for execution by a set of worker threads based on constraints provided by the user, either in the form of dependencies or dataflow expressions.
At the same time, MPI is still the dominant interface for interprocess communication in parallel applications [6] , providing blocking and non-blocking point-to-point and collective operations as well as I/O capabilities on top of elaborate datatype and process group abstractions [37] . Recent years have seen significant improvements to the way MPI implementations handle communication by multiple threads of execution in parallel [20, 41] .
However, task-based programming models pose new challenges to the way applications interact with MPI. While MPI provides non-blocking communications that can be used to hide communication latencies, it is left to the application layer to ensure that all necessary communication operations are eventually initiated to avoid deadlocks, that in-flight communication operations are eventually completed, and that the interactions between communicating tasks are correctly handled. Higher-level tasking approaches such as PaRSEC or HPX commonly track the state of active communication operations and regularly test for completion before acting upon such a change in state. With node-local programming models such as OpenMP, this tracking of active communication operations is left to the application layer. Unfortunately, the simple approach of test-yield cycles inside a task does not provide optimal performance due to CPU cycles being wasted on testing and (in the case of OpenMP) may not even be portable [50] .
MPI is thus currently not well equipped to support users and developers of asynchronous programming models, which (among other things) has prompted some higher level runtimes to move away from MPI towards more asynchronous APIs [10] .
Different approaches have been proposed to mitigate this burden, including Task-Aware MPI (TAMPI) [46] and a tight integration of user-level thread, fiber, and tasking libraries (cf. Section 2) with MPI implementations [32, 34, 45] . In this paper, we argue that the latter is the least preferable solution and in fact should be seen critical by the MPI community. 1 After establishing a taxonomy to be used throughout this paper (Section 2), we will make the argument that such an integration is neither warranted by the MPI standard nor beneficial to the goal of supporting the development of portable MPI applications (Section 3). Instead, we propose adding continuations to the MPI standard to facilitate a loose coupling between higherlevel concurrency abstractions and MPI (Section 4). We will show that the interface is flexible and can be used with abstractions on different levels, including OpenMP detached tasks, OmpSs-2 task events, and Argobots (Section 5).
Unfortunately, the term thread (or thread of execution) is not welldefined in the computer science literature. Terms like kernel threads, (light-weight) processes, user-level and user-space threads, POSIX threads (pthreads), light-weight threads, and fibers are used without a clear consensus on a common taxonomy. Establishing such a taxonomy is thus warranted to better guide the discussion in the remainder of this paper. Some of the description is based on the terminology provided in [59] and [52] . The relation between the concepts is depicted in Figure 1 , described here from bottom to top.
A processor provides the active physical resources to execute a stream of instructions. In the context of this work, the term referes to a unicore CPU or a single core on a multi-core CPU.
Kernel Thread. A kernel thread is a thread of execution that is entirely controlled by the operating system kernel. Kernel threads only execute within the kernel space and serve tasks such as asynchronous I/O, memory management, and signal handling. They also form the basis of light-weight processes.
A process is an execution instance of a program and a user-level abstraction of state, providing isolation of kernel-level resources such as memory pages and file pointers between different processes. A process may use one or more light-weight processes.
Light-weight Process (LWP). A light-weight process can be considered an abstraction of a processor inside the operating system (OS), on which threads are scheduled. LWPs are scheduled preemptively by the OS kernel, i.e., their execution may be preempted and another LWP be scheduled onto the used processor. Different LWPs may share resources such as file descriptors and memory if they belong to the same process.
Cooperative and Preemptive Scheduling. These are two main scheduling strategies to distinguish. With cooperative scheduling, a scheduler assigns resources to a process or thread until it relinquishes them. The benefits of cooperative scheduling are lower scheduling overheads (scheduling decisions are made only at welldefined points) and-at least on single-core systems-atomicity of execution between scheduling points [1] . With preemptive scheduling, the scheduler is able to preempt the execution of a thread after the expiration of a time-slice or to schedule processes or threads with higher priority. Most modern OS kernels employ preemptive scheduling to ensure fairness and responsiveness.
User Thread. User threads are threads of execution that share resources within the confines of a process. threads may be statically assigned to LWP (1:1 mapping) or multiplexed onto either a single (1:N mapping) or LWPs (N:M mapping) by a user thread scheduler. In all cases, threads are comprised of some private state (stack memory, register context, thread-local variables, signal masks, errno value, scheduling priority and policy) and have access to resources shared between threads within a process (heap memory, global variables, file descriptors, etc).
A common example of a user thread implementation are POSIX threads (pthreads) [24] . In all currently available Unix flavors known to the authors, a 1:1 mapping between pthreads and LWPs is maintained, including Linux [31] and various BSD flavors [55] [56] [57] . They are the basic building block for any type of multi-threaded applications and runtime systems. Solaris provided its own thread implementation (Solaris threads), which originally employed an N:M mapping that was abandoned eventually for efficiency reasons [54] , as were previous N:M implementation in BSD. POSIX, Solaris, and Windows threads [35] are scheduled preemptively. 2 We will refer to user threads as defined here simply as threads in the remainder of this work.
Fiber. Fibers differ from threads in that multiple fibers share some of the context of the thread they are executing on, i.e., they are not fully independent execution entities [16] . A fiber is comprised of a private stack and a memory region to save relevant registers when the fiber is rescheduled. However, fibers executing on the same thread have access to the same thread-private variables and share the priority and time-slice of the thread. They are unknown to the OS, i.e., they do not posses a thread context assigned by the OS or user thread scheduler, and are cooperatively scheduled for execution on threads by simply changing the stack pointer and adjusting the register context. A fiber blocked in a library call will block the thread until the operation has completed. A fiber blocked in a system call will cause the executing thread to be blocked (and potentially rescheduled in lieu of another thread).
Fiber implementations may provide synchronization primitives that reschedule the fiber to allow the underlying thread to execute a different fiber if the execution would otherwise be blocked. However, third-party libraries have to actively support a particular fiber implementation. While fibers are a way to express concurrency, their use does not impact the parallelism of an application, which is a function of the number of available processors and the number of threads used to execute fibers.
A wide range of fiber implementations is available: in Microsoft Windows [36], the Boost.Fiber C++ library [28] , the GNU pth package [14] 3 , a custom implementations in OmpSs [13] , Qthreads [61] , as well as the more recent Argobots [51] . The POSIX makecontext set of functions can be used to implement fibers [30] .
The main difference between fibers and threads should be seen in that a thread is the lowest executing entity in user space while a fiber may provide some execution context and isolation but always requires an underlying thread for execution.
In the literature, fibers are also referred to as lightweight threads and user-level threads [51, 61] . However, we believe that the term thread (esp. in light of the ensuing discussion) is convoluted and a clear distinction between threads and fibers is necessary.
A task is a unit of work that is scheduled for execution by a task scheduler, potentially based on constraint (e.g., dependencies) provided by the user. The task scheduler maps the execution of a set of tasks to a set of worker threads, which may execute the associated action either directly on their stack or encapsulate the execution within a fiber to allow for rescheduling of executing tasks. Note that the task and fiber schedulers depicted in Figure 1 may be collapsed into a single instance managing both aspects.
In the discussion of whether fiber implementations should be tightly integrated with MPI implementations, we will look at three aspects: i) correctness (of the MPI implementation and the application); ii) portability; and iii) potential performance issues.
When considering correctness in the context of thread-parallel MPI it is important to differentiate between MPI-level correctness and application-level correctness.
The MPI standard mandates that thread-compliant implementations should a) be thread-safe; and b) only block the calling thread of a "thread package similar to POSIX threads" [37, §12.4] . Threadsafety requires MPI implementations to protect any internal data structures from corruption in case multiple threads make calls into 3 Note that the GNU Pth author distinguishes between kernel-space and user-space threads to distinguish 1:1 and 1:N mappings between threads and LWPs and mentions the possibility of hybrid forms. We feel that this distinction is confusing with regards to the existence of kernel threads described above. the MPI library in parallel, either calling into the same or different procedures. This is commonly achieved through mutual exclusion devices provided by the underlying threading library, e.g., using instances of pthread mutex, or by using operations to atomically modify values in memory, which are provided by the processor.
More interesting, however, is the second requirement: blocking operations should only block the calling thread and allow other threads to continue progressing their operations. This is especially important in cases where the successful completion of one thread's operation depends on the completion of another thread's operations, either directly (thread A sending a message to thread B on the same process) or indirectly (thread 0 sending a message to thread 1 on a different process, which then responds with a message that thread 0 is waiting on). In the context of POSIX threads, this can be achieved by reducing the time mutexes are held.
Part of the correctness requirement of a thread-compliant MPI implementation is the avoidance of deadlocks at the implementation level, e.g., by avoiding lock order inversion. However, MPI implementations are not required to ensure that all threads are scheduled for execution to avoid deadlocks at the application level since they are scheduled preemptively: once a thread blocked in an MPI call exceeds its time-slice it will be replaced by another thread that may then initiate its communication. Eventually, all threads will have initiated their communication and the application progresses.
Listing 1 provides an example of multiple threads being spawned waiting to receive a message through MPI from the main thread. A schematic depiction of parts of the execution is shown in Figure 2a . This example will always succeed, even for large numbers of receiving threads, due to the preemptive scheduling of POSIX thread implementations. It is thus a correct MPI program.
MPI implementations may employ thread synchronization mechanisms to improve the efficiency of multi-threaded MPI applications but such optimizations are not required for correctness. In Listing 1, we can replace the used blocking with non-blocking MPI procedures and a tight test-loop to prevent such optimizations inside the MPI implementation. The result will be the same. Replacing the calls to pthread_create in Listing 1 with the respective calls in a fiber library, however, would not yield a correct program even if the MPI implementation called sched_yield. Depending on the scheduling order of the fibers, the application may deadlock because fibers may be executed for which the main thread does not currently send a message. The application has to be considered erroneous if it cannot guarantee that all relevant communication operations are initiated. 4 Such an erroneous execution is depicted in Figure 2b , where the thread executing the receiving fiber (orange) is blocked indefinitely inside MPI and the sending fiber (yellow) is never scheduled for execution.
It should be noted that the behavior of the MPI implementation in that case is perfectly legitimate: since fibers are executed by threads, the implementation may use mutexes to guard its internal data structures against corruption from two or more threads making MPI calls in parallel. The correctness of the application's communication pattern is beyond the scope of the MPI implementation.
Correctness may be the main argument for a tight integration of (some) fiber libraries into MPI implementations to ease the burden for users. However, the MPI standard does not mandate MPI implementations to support threading libraries beyond the basic building blocks (such as pthreads). Thus, having two implementations-one with basic pthread support ( Figure 2b ) and one with support for the used fiber library (Figure 2c )-results in a situation in which both libraries are correct (from the MPI standard's viewpoint) but only one produces a correct outcome. Hence, applications using blocking MPI communication inside fibers have to rely on an implementation detail to ensure correct execution. This raises significant concerns regarding the portability of MPI applications employing fibers.
Portability is a major goal of the MPI standard, which mentions the term portable at least 88 times-once every ten pages-and explicitly states that "the interface should establish a practical, portable, efficient, and flexible standard for message-passing. " [37] 4 Such behavior would be comparable with Example 3.8 of the MPI standard, which calls an attempt to use blocking receive calls on two communicating processes before initiating the send operations errant [37, p. 43] . It is thus important for any solution tackling the issue of taskparallel MPI communication to provide means that are portable across multiple implementations.
Listing 2 shows a version of the example provided in Listing 1 using OpenMP tasks. As soon as NUM_TASKS exceeds the number of threads available in the OpenMP parallel section this application may deadlock with most combinations of MPI+OpenMP implementations. Table 1 provides an overview of such combinations under which this application may run to completion for arbitrary NUM_TASKS, i.e., won't deadlock. Note that the positive combination of BOLT [26] and Open MPI/MPICH [32] is not unconditional: at the time of this writing, the support for Argobots (and Qthreads) in both MPI implementations depends on a compile-time configuration switch of the MPI library. Applications would thus have to rely on either building their own MPI library or on the respective system administrators to provide-and on users to choose-the installation with support for the correct fiber library enabled. A major issue for applications relying on the availability of fiber support in MPI libraries is that this information is not exposed to the application layer, i.e., an application has no way of knowing whether the underlying MPI library was configured appropriately.
By offering such integration in releases of mainstream MPI implementations, the community is actively inviting application developers to start relying on such tight integration, fostering the development of non-portable MPI applications. The resulting incompatibilities and efforts for debugging deadlocks will affect a wide range of the MPI community. If, however, users were not encouraged to actually use these features the whole effort would be moot and the integration-including the disruptions to the threading support that is part of an implementation's core, the required testing, and efforts for integration into additional third-party libraries-was entirely unnecessary. Either way, the efforts to integrate fiber models into mainstream releases of MPI libraries should be considered harmful.
One of the major differences in the use of threads and fibers is oversubscription: while applications and runtime systems typically try to avoid the use of more worker threads than available processors to mitigate the negative consequences of kernel-level scheduling, oversubscription is at the heart of the use of fibers. Thanks to the low-overhead switching between fibers entirely in user-space, it is 
easily possible to create more fibers than worker threads and hide I/O-related latencies, including communication and file I/O. However, if low latencies in the execution of a sequence of operations are paramount to the overall performance of an algorithm, unsolicited context switching inside MPI may negatively impact overall application performance. As an example, application-level communication protocols using MPI RMA may rely on low-latency accumulate operations [48] . Any delay may result in unnecessarily increased latencies with the application layer having no way to control the context switching initiated by the MPI implementation. While many communication latencies should be hidden through fiber oversubscription, some can be tolerated and it should be up to the application to decide between the two cases.
Instead of tightly integrating various asynchronous execution libraries, we advocate for a mechanism to accomplish loose coupling between MPI and the higher-level concurrency abstractions.
Continuations are a concept for structuring the execution of different parts of an application's code, dating back to research on Algol60 [15, 44] . They have recently been proposed to the C++ standard in the form of std::future::then to coordinate asynchronous activities [18] . Continuations consist of a body (the code to execute) and a context (some state passed to the body) on which to operate.
A similar mechanism for MPI can be devised that allows a callback to be attached to an MPI request, which will be invoked once the operation represented by the request has completed. This establishes a notification scheme to be used by applications to timely react to the completion of operations, e.g., to wake up a blocked fiber or release the dependencies of a detached task, all while relieving applications of managing MPI request objects themselves.
We propose a set of functions to set up continuations for active MPI operations, which may be invoked by application threads during the execution of communication-related MPI functions or by an implementation-internal progress thread (if available) once all relevant operations are found to have completed. The body of a continuation may call any MPI library function and thus start new MPI operations in response to the completion of previous ones.
The MPI Continuations API consists of two parts. First, continuation requests are used to aggregate and progress continuations. Second, continuations are then attached to active MPI operations and registered with continuation requests for tracking. /* Attach a continuation to an active operation represented by * op_request . Upon completion of the operation , the callback * cb will be invoked and passed status and cb_data as arguments . * The status object will be set before the continuation is invoked . * If the operation has completed already the continuation will * not be attached or invoked and flag will be set to 1 
A continuation is attached to one or several active operations and registered with the continuation request for tracking. A call to MPIX_Continue attaches a continuation to a single operation request while the use of MPIX_Continueall sets up the continuation to be invoked once all operations represented by the provided requests have completed. As shown in Listing 3, the continuation is represented through a callback function with the signature of MPIX_Continue_cb_function (provided as function pointer cb) and a context (cb_data). Together with the pointer value provided for the status or statuses argument, the cb_data will be passed to cb upon invocation. Upon return, all provided non-persistent requests are set to MPI_REQUEST_NULL, effectively releasing them back to MPI. 5 No copy of the requests should be used to cancel or test/wait for the completion of the associated operations. Consequently, only one continuation may be attached to a non-persistent operation. In contrast, persistent requests may still be canceled, tested, and waited for. More than one continuation may be attached to a persistent request but the continuations themselves are not persistent.
Similar to MPI_Test, flag shall be set to 1 if all operations have completed already. In that case the continuation callback shall not be invoked by MPI, leaving it to the application to handle immediate completion (see Section 4.5.1). Otherwise, flag is set to zero.
Unless MPI_STATUS_IGNORE or MPI_STATUSES_IGNORE has been provided, the status object(s) will be set before the continuation is invoked (or before returning from MPIX_Continue[all] in case all operations have completed already). The status objects should be allocated by the application in a memory location that remains valid until the operation has completed and the callback has been invoked, e.g., on the stack of a blocked fiber or in memory allocated on the heap that is released inside the continuation callback.
An example of a potential control flow between tasks and the MPI implementation is sketched in Figure 3 . Task T1 (blue) is calling MPI_Isend and MPI_Irecv before attaching a continuation to the resulting requests. The task is then blocked by means provided by the tasking library, allowing a second task T2 (green) to be scheduled for execution. After some computation, T2 calls MPI_Isend, in which the MPI library determines that both previously created requests have completed. Consequently, the continuation is invoked in the context of T2 from within the call to MPI_Isend, which releases the blocked task T1. After returning from the MPI call, T2 attaches a continuation to its resulting request and blocks, allowing the execution of the just released task T1 to resume.
The example provided in Listing 4 uses MPI Continuations to release detached tasks in an OmpSs-2 [9] application once communication operations have completed. Looping over all fields of a local domain in each timestep, one task is created per field, which carries an output dependency on its field object (Line 19). Inside each task, the received boundary data from the previous timestep is incorporated and a solver is applied to the field (potentially with nested tasks) before the local boundary is packed for sending (Lines 22-24).
Both the send and receive operations are initiated and a continuation is attached using MPIX_Continueall (Lines 27-37). OmpSs-2 offers direct access to API functions of the underlying nanos6 runtime. In this case, the event counter for the current task is retrieved, increased, and passed as the context of the continuation. If all operation completed immediately, the event counter is decremented again. 6 Otherwise, the task will run to completion but its dependencies will not be released before the event counter is decremented in the continuation callback continue_cb (Lines 6-9).
In order to ensure that all continuations are eventually completed, a polling service is registered with the OmpSs-2 runtime (Line 15). This polling service is regularly invoked by the runtime and-in the function mpi_poll_service in Lines 1-5-calls MPI_Test on the continuation request. 7 This will invoke all available continuations and eventually cause all tasks of the next timestep to become available for execution once the respective send and receive operations of the previous timestep have completed.
With only approximately 15 lines of code (including setup and tear-down), it is possible to integrate MPI communication with a task-parallel application to fully overlap communication and computation. While this is, naturally, more code than would be required with TAMPI or fiber support in MPI, it is fully portable and allows an application to manage application-level concerns while leaving MPI-level concerns such as request management to MPI. By making the interaction between non-blocking MPI operations and the task scheduler explicit, this approach ensures that both MPI and the task programming system support all required operations.
A scheme similar to the one proposed here could be implemented outside of the MPI library with an interface similar to TAMPI. However, a main advantage of the integration with MPI is that the continuations can be invoked as soon as any thread calls into MPI and determines the associated operations to be complete. In more complex applications, this allows for the invocation of continuations set up by one part of the application during an MPI call issued in another part of the application, potentially reducing the time to release of blocked tasks and thus preventing thread starvation. should be avoided as it may cause a call to a nonblocking MPI procedure to block on an unrelated operation. No other continuation may be invoked in MPI calls made within a continuation. Continuations may not be invoked from within signal handlers inside the MPI implementation as that would restrict the application to using only async-signal-safe operations. MPI implementation relying on signals to interact with the network hardware should defer the execution to the next invocation of an MPI procedure by any of the application threads or hand over to a progress thread.
Unlike existing request types in MPI, continuation requests (CR) do not represent a single operation but a set of operations. Extending the semantics of persistent requests [4] , Figure 4 depicts the state diagram of CRs. That state may change with every new registration or completion of a continuation. A newly Initialized or otherwise Inactive CR becomes Active Referenced when a continuation is registered. It remains Active Referenced if further continuations are registered. Upon completion of continuations, they are deregistered from the CR. The CR becomes Active Idle upon the deregistration of the last active continuation. An Active Idle CR may become either Active Referenced again if a new continuation is registered or Complete if a Completion function such as MPI_Test is called on it. It is possible to call MPI_Request_free on an active CR, in which case the CR cannot be used to register additional continuations and will be released as soon as all previously registered continuations have completed.
A continuation may be attached to a CR itself and registered with a separate CR, allowing applications to build complex patterns of notifications. The new continuation will then be executed once all continuations registered with the first CR have completed.
Multiple threads may safely register continuations with the same continuation request in parallel but only one thread may test or wait for its completion at any given time. This allows for continuations to be registered concurrently without requiring mutual exclusion at the application level.
The availability of continuations is only one piece of the puzzle to reliably and portably integrate MPI with task-based applications. The issue of progress in MPI is long-standing and while some implementations provide internal mechanisms for progressing communication (e.g., progress threads) this is not behavior users can rely on in all circumstances. Thus, applications are still required to call into MPI to progress outstanding communication operations and-with the help of the proposed interface-invoke available continuation callbacks. This proposal does not change the status quo in that regard.
It is thus left to the application layer to make sure that communication is progressing. This can be achieved by regularly testing the continuation request, which could be done either through a recurring task, a polling service registered with the task runtime system (as shown for OmpSs-2 in Listing 4), or by relying on a progress thread inside MPI. The interface presented here is flexible enough to allow for its use in a wide range of circumstances and it defers to the application layer to choose the right strategy based on the available capabilities.
We will evaluate the proposed interface using the OSU latency benchmark. We have ported its multi-threaded benchmark to use Argobots instead of pthreads. We will then discuss a port of the NPB BT-MZ benchmark to C++ to compare TAMPI with MPI continuations used with both OmpSs-2 and OpenMP detached tasks. All measurements were conducted on Vulcan, a small production NEC Linux cluster employing dual-socket 12-core Intel Xeon E5-2680v3 (Haswell) nodes connected using Mellanox ConnectX-3 cards. 8 The configurations of the used software are listed in Table 2 .
All data points represent the mean of at least five repetitions, with the standard deviation plotted as error bars in the graph.
We have implemented a proof-of-concept (PoC) of continuations within Open MPI. 9 The fast-path for regular requests without a registered continuation adds 12 instructions over vanilla Open MPI (+2% on a zero-byte message sent and received by the same process measured using PAPI). Registration and invocation of an empty continuation requires 300 instructions in this scenario. This is reflected in Figure 5 , which shows the non-blocking point-to-point latency measured for two processes on two different nodes, comparing the Open MPI master branch against the PoC using traditional non-blocking send/recv followed by a wait as well as the use of continuations to send or receive the reply. There is virtually no difference between vanilla Open MPI and the PoC for non-blocking communication and a slight uptick in latency when using continuations (about 4% for single-byte messages). The difference vanishes for larger messages.
In order to directly compare the continuations interface with the Argobots integration, we ported the multi-threaded OSU latency P2P benchmark (osu_latency_mt) to use Argobots instead of POSIX threads with blocking communication. 10 The code used to block and unblock Argobot fibers when using Continuations with nonblocking communication is listed in Listing 5.
The current Argobots integration in Open MPI does not employ Open MPI's synchronization mechanisms and instead relies on yielding inside the progress engine. We thus had to configure the UCX PML to trigger the global progress engine unconditionally if (! flag ) ABT_cond_wait (fs -> cond , fs -> mtx ) ; ABT_mutex_unlock (fs -> mtx ); } int unblock_fiber ( MPI_Status * status , void * data ) { fiber_state_t * ts = ( fiber_state_t *) data ; ABT_mutex_lock (fs -> mtx ); ABT_cond_signal (fs -> cond ); ABT_mutex_unlock (fs -> mtx ); return MPI_SUCCESS ; } Listing 5: Code to block and unblock an Argobot fiber. The fiber_state_t structure contains a fiber-specific mutex and conditional variable. Locking the mutex in unblock_fiber is necessary to prevent signals from getting lost.
by passing -mca pml_ucx_progress_iterations 1 to mpirun as otherwise the latencies would have been an order of magnitude higher. We have achieved similar latencies with a test-yield cycle inside the benchmark instead of using blocking communication.
The results are depicted in Figure 6 with different combinations of worker threads (execution streams) and fibers. With a single thread and a single fiber (Figure 6a ), the overhead of using conditional variables induces higher latencies than the yielding in Open MPI's Argobots integration, leading to 23% higher latencies for small messages. This effect vanishes for larger messages starting at 32 KB. However, with 12 worker threads and one fiber per thread (Figure 6b ), the use of conditional variables with continuations yields more than 2× lower latency than the yielding in Open MPI and 3× lower latency than MPICH for small messages.
Scaling the number of fibers shown in Figure 6c and Figure 6d follows a similar trend: while for a single worker-thread using yield seems to be more efficient, the use of conditional variables is more efficient with 12 worker threads. In contrast to the MPI implementation, the application has full knowledge of its configuration and can choose between yielding and blocking fibers.
In this section, we demonstrate the use of MPI Continuations in the context of the NAS Parallel Benchmark application BT-MZ, a Figure 6 : Argobots port of the osu_latency_mt P2P benchmark with different numbers of threads and fibers.
multi-zone block tri-diagonal solver for the unsteady, compressible Navier Stokes equations on a three-dimensional mesh with twodimensional domain decomposition [11] . Zones of different sizes are distributed across processes based on a static load-balancing scheme. In each timestep and for each local zone, five computational steps are involved: forming the right-hand side, solving block-diagonal systems in each dimension x, y, and z, and updating the solution. We use two representative problem sizes-classes C and D-for which the input configurations are listed in Table 3 . The reference implementation in Fortran uses OpenMP work-sharing constructs to parallelize nested loops during the updates to all local zones before collecting and exchanging all boundary data at the end of each timestep. OpenMP parallelization is done over the outermost loop, which in most cases is the smallest dimension z, with the notable exception of the solution in the z dimension itself.
We have ported the Fortran version to C++ and implemented two variations using task-based concurrency. 11 The first variation uses tasks to overlap the computation of zones within a timestep in a fork-join approach in between boundary updates, replacing OpenMP parallel loops with loops generating tasks, with their execution coordinated using dependencies. The second variant extends this to also perform the boundary exchange inside tasks, including the necessary MPI communication, effectively minimizing the coupling between zones to the contact point dependencies.
The second task-based variant requires some support from the tasking library to properly handle communicating tasks. We use 12 We have attempted to use taskyield within OpenMP tasks using the Clang implementation, which resulted in deadlocks due to the restricted semantics of task-yield in OpenMP [50] . The implementation using detached tasks spawns a progress thread that tests the continuation request before calling usleep to yield the processor. Given the limited concurrency in the OpenMP loops, we present the minimum of the average runtime of 5 repetitions achieved using 1, 2, or 4 processes per nodes (24, 12, and 6 threads, respectively). All codes have been compiled with the flags -O2 -march=haswell -mcmodel=medium. The C++ variants were compiled using the Clang compiler while the Fortran variant was compiled using GCC's gfortran. OpenMP thread affinity was enabled (OMP_PROC_BIND=true). Process binding has been achieved using Open MPI's -map-by node:PE=$nthreads -bind-to core arguments to mpirun. Figure 7 provides results for classes C and D. Most notably, the task-based variants with fine-grained MPI communication (OpenMP detached tasks, OmpSs-2) outperform the variants using OpenMP loop-based worksharing and bulk communication by at least 15% (C) or 24% (D) at scale. It is also notable that the OmpSs-2 variants yield by far the highest performance, even though their task-structure is similar to that using OpenMP detached tasks. We attribute this to potential degradation of parallelism in Clang's libomp if tasks waiting in taskwait constructs (required to wait for nested tasks) are stacked up on the same thread, similar to issues reported for the taskyield directive [50] . 13 The usage of MPI Continuations with OmpSs-2 provides a slight speedup of about 1.5% over TAMPI for class D, suggesting a more efficient communication management.
Multiple efforts have been made in the past to improve specific aspects of the interaction between multi-threaded programming models and MPI [12, 17, 22] .
Extended generalized requests have been proposed to encapsulate operations within requests and allow the MPI implementation to progress them through a callback [29] . Similar to the interface proposed here, extended generalized requests provide inversion of control (IoC) [33] : application-level code is called by the MPI library in inversion of the usual call relation. However, these requests are still polling-based and cannot be used for completion notification.
IoC is also used in the upcoming MPI_T events interface, allowing applications and tools to be notified through callbacks about events from within the MPI library [19] . However, the availability of events is implementation-specific and callbacks can only be registered for event types, as opposed to specific operations. The MPI_T interface has been used to structure the interaction between MPI and taskbased programming models, still requiring the mapping between MPI requests and task-information in the application layer [8] .
Continuations are not a new concept in the context of low-level communication libraries: UCX allows passing callback notification functions when initiating a non-blocking operation [58] . In contrast, other interfaces such as libfabric [39] , Portals [5] , and uGNI [42] rely on completion queues from which notifications are popped. While this would be a viable concept in MPI as well, we believe that continuations provide greater flexibility at the application level.
Another alternative would be a system similar to scheduler activations in OS kernels [2] , where MPI would signal to some applicationlevel entity that the current thread or fiber would block inside a call. This has been proposed for the OpenShmem standard [60] . However, its use is limited to tasking models that have strong rescheduling guarantees and would not work with OpenMP detached tasks.
Hardware-triggered operations are being used to react to events in the network with low latency and to offload computation to the network interface card [23, 25, 47] . However, in-network computing is focused on operations on the data stream and likely cannot be utilized to execute arbitrary code in user-space, e.g., call into fiber libraries. Exploring the boundaries of integrating these systems with the interface proposed here remains as future work.
The integration of fiber libraries into MPI has been proposed as a replacement for POSIX thread support to enable the use of blocking MPI calls inside tasks and fibers [32] . This has been discussed in Section 3. Other proposals provide runtime-specific wrapper libraries around MPI, transforming blocking MPI calls to non-blocking calls and testing for completion before releasing a fiber or task [45, 46, 53] . These proposals fall short of providing a portable interface that can be used with arbitrary asynchronous programming models.
A proposal for completion callbacks for MPI request objects has been discussed in the context of the MPI Forum over a decade ago and rejected due to its broad focus [21] . In contrast, the proposal outlined here avoids some of the pitfalls such as callback contention, ownership of requests, and unclear progress semantics.
The interface proposed here is especially well-suited for nodelocal asynchronous programming models such as OpenMP [40] , OmpSs-2 [9] , and Intel TBB [43] or in combination with cooperatively scheduled fiber libraries such as Argobots [51] or Qthreads [61] to ease the communication management burden users are facing. However, it may also be useful in distributed runtime systems such as PaRSEC [7] , HPX [27] , StarPU [3] , and DASH [49] to simplify internal communication handling. An evaluation of the integration with runtime systems remains as future work.
This paper has made the argument that the integration of fiber libraries into MPI implementations for the sake of application programmability is neither warranted by the MPI standard nor beneficial in terms of maintaining an ecosystem of portable applications. In order to tackle the challenges of asynchronous activities communicating through MPI, we have proposed an extension to the MPI standard that allows users to register continuations with active operations, which will be invoked once the MPI library determines the operation to be complete. We have shown that this interface is easy to use and flexible enough to accommodate three different tasking and fiber models (OpenMP detached tasks, OmpSs-2, and Argobots). The overhead of our proof-of-concept implementation is sufficiently low to not impact existing applications and its use shows similar or improved performance over existing approaches.
The interface has been designed with higher-level abstractions such as C++ futures for MPI in mind but it remains as future work to provide an actual implementation on top of MPI Continuations. It is also an open point of discussion whether it is desirable to allow users to limit the context in which certain continuations may be invoked to mitigate the impact of expensive continuations on latency-sensitive parts of the application.