Add gc_safe option to @mpicall and @mpichk macros - #955
Conversation
Extend `@mpicall` and `@mpichk` to accept an optional leading
`gc_safe=(true|false)` argument, like the `@ccall` macro, while keeping
the functional `ccall(...)` syntax:
@mpicall gc_safe=true ccall((:MPI_Fn, libmpi), ...)
@mpichk gc_safe=true ccall((:MPI_Fn, libmpi), ...) [min_version]
When `gc_safe=true`, the garbage collector is allowed to run
concurrently with the foreign call, which is useful for MPI functions
which may block. This is implemented by injecting the same
`Expr(:cconv, (convention, effects, gc_safe), nreq)` marker produced by
the lowering of `@ccall gc_safe=true` (`ccall_macro_lower` in
`base/c.jl`) into the calling-convention slot of the `ccall` expression,
using the `stdcall` convention for Microsoft MPI as before. The option
is only supported by Julia v1.12+ and silently ignored on older
versions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In Clang.jl v0.19 the `callback_documentation` option is invoked with the docstring extracted from the C comments as second argument, instead of the expression node alone. Also, compute the path of the generator file shown in the warning header of the generated file with `relpath`, instead of a regex replacement which only worked when the repository checkout was named `MPI.jl`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a tuple of functions to be called with the `gc_safe=true` option, to let the garbage collector run concurrently with the (potentially blocking) MPI call, and use it in the post-processing of the generated bindings. Start with `MPI_Wait` and `MPI_Allreduce`, more functions can be added in the future if necessary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary of the changes: * all 556 previously wrapped functions have identical definitions, the large diff is mostly due to the different order in which the declarations are emitted; * 49 new functions coming from the MPICH 5.0.1 headers are added (e.g. the `MPI_Abi_*` family); * `MPI_Wait` and `MPI_Allreduce` are now called with the `gc_safe=true` option, to let the garbage collector run concurrently with these potentially blocking calls (only on Julia v1.12+, the option is ignored on older versions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clang.jl emits the function wrappers in the same order as they are declared in the parsed header, but for MPICH `mpi_proto.h` is itself an auto-generated file whose ordering is not stable across releases, resulting in large spurious diffs when regenerating the bindings. Sort the blocks (docstring + function definition) by name of the function in the post-processing, to make the output of the generator deterministic: future regenerations will only show differences where a signature actually changed. Regenerate the bindings accordingly: this reordering is a one-time diff, the content of the file is unchanged (verified block-by-block), and regenerating again produces a byte-identical file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With Microsoft MPI the plain form carries the `:stdcall` calling convention in the same slot where the `gc_safe=true` form has the `Expr(:cconv, ...)` marker, so the convention has to be removed from both expressions before comparing their remaining arguments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| gc_safe = ( | ||
| :MPI_Wait, | ||
| :MPI_Allreduce, | ||
| ) |
There was a problem hiding this comment.
@vchuravy for the time being I marked only
MPI_WaitandMPI_Allreduce, do we need to support more functions?
The bot suggests
gc_safe = (
# request completion and probes
:MPI_Wait, :MPI_Waitall, :MPI_Waitany, :MPI_Waitsome,
:MPI_Probe, :MPI_Mprobe, :MPI_Mrecv,
# blocking point-to-point
:MPI_Send, :MPI_Ssend, :MPI_Recv, :MPI_Sendrecv, :MPI_Sendrecv_replace,
# blocking collectives
:MPI_Barrier, :MPI_Bcast,
:MPI_Gather, :MPI_Gatherv, :MPI_Scatter, :MPI_Scatterv,
:MPI_Allgather, :MPI_Allgatherv, :MPI_Alltoall, :MPI_Alltoallv, :MPI_Alltoallw,
:MPI_Reduce, :MPI_Allreduce, :MPI_Reduce_scatter, :MPI_Reduce_scatter_block,
:MPI_Scan, :MPI_Exscan,
# blocking neighborhood collectives
:MPI_Neighbor_allgather, :MPI_Neighbor_allgatherv,
:MPI_Neighbor_alltoall, :MPI_Neighbor_alltoallv, :MPI_Neighbor_alltoallw,
)There was a problem hiding this comment.
The only thing I am worried about is the MPI_Probe since they should be very fast.
There was a problem hiding this comment.
The bot really insisted that probes are fine
MPI_Mrecv— the one I nearly cut. AfterMPI_Mprobethe message is matched, so it "should" be a bounded copy. But matching only guarantees the envelope arrived: for a large rendezvous message the payload transfer happens insideMPI_Mrecv, and it needs the sender's MPI library to answer the clear-to-send. MPI implementations don't guarantee asynchronous progress — if the sender rank is off in compute code, that answer waits until it re-enters MPI. Unbounded tail. Keep.[...]
MPI_Probeis one of the strongest candidates on the list — it sits at the opposite end of the spectrum fromMPI_Mrecv.On the "worth it" test: its entire job is to block until a matching message envelope arrives, i.e. until a peer rank posts the corresponding send. That's a pure peer-progress wait with no local-work bound at all — there isn't even a fast typical case to speak of, since code reaches for
MPI_Probeprecisely when it doesn't know when (or how big) the message will be. A rank sitting inMPI_Probewaiting for a slow peer while holding up GC on every other thread is exactly the motivating pathology.On the safety test: it's about as inert as an MPI call gets — it doesn't touch user buffers at all, writes only a bits-type
MPI_Status, receives nothing, and has no callback path (unlikeMPI_Wait, it doesn't complete other pending operations' reduction ops; it only inspects the matching queue and drives progress internally).So: keep, with confidence. Same verdict for
MPI_Mprobe, which is the same wait with a matched-message handle as output.
In any case I don't have a need for marking these functions as gc-safe, so I dropped them.
Extend the tuple of functions called with `gc_safe=true` from just `MPI_Wait` and `MPI_Allreduce` to all 34 wrapped functions whose worst-case duration is unbounded because they wait on the progress of peer ranks: request completion (`MPI_Wait*`) and blocking probes (`MPI_Probe`/`MPI_Mprobe`/`MPI_Mrecv`), blocking point-to-point communications, and blocking (neighborhood) collectives. Functions whose worst case is bounded by local work (e.g. `MPI_Bsend`, `MPI_Test*`, `MPI_Iprobe`, the nonblocking initiators) gain nothing from the option and are left unmarked. The only way any of the marked functions can re-enter Julia is through `@cfunction` callbacks (custom reduction operators), which are safe in `gc_safe` regions. Regenerate the bindings accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| ## Multi-threading and garbage collection | ||
|
|
||
| On Julia v1.12 and later, blocking MPI calls (e.g. `MPI_Wait`, `MPI_Barrier`, blocking point-to-point communications and collectives) are marked as *GC-safe*: the Julia garbage collector running on one thread does not have to wait for other threads which are inside one of these MPI calls. | ||
| This prevents the whole process from stalling — or deadlocking outright — when a thread blocks in MPI while waiting on the progress of other ranks and another thread needs to run a garbage collection. | ||
|
|
||
| A consequence of this is that in a multi-threaded program the garbage collector can now run *concurrently* with a blocking MPI call, and its finalizers can free MPI handles (communicators, requests, datatypes, operators, etc.) by calling functions like `MPI_Comm_free` while another thread is still inside MPI. | ||
| Concurrent MPI calls from different threads are only permitted when MPI is initialized with the `MPI.THREAD_MULTIPLE` [`ThreadLevel`](@ref), whereas [`MPI.Init`](@ref) defaults to `:serialized`. | ||
| Multi-threaded programs should therefore initialize MPI with | ||
|
|
||
| ```julia | ||
| MPI.Init(; threadlevel=:multiple) | ||
| ``` | ||
|
|
||
| Single-threaded programs are unaffected. | ||
|
|
There was a problem hiding this comment.
@vchuravy this was entirely the bot's initiative, I'm not quite sure whether it's true that multi-threaded applications need
MPI.Init(; threadlevel=:multiple)to make the GC run concurrently with the MPI functions. I'm happy to remove this if it's garbage (pun intended).
There was a problem hiding this comment.
Oh that is fun... it is an interesting question what happens when we run MPI_*_free from a finalizer...
On Julia v1.12+ blocking MPI calls are now marked as gc-safe, so the garbage collector no longer has to wait for threads which are inside one of these calls, preventing stalls or deadlocks when a thread blocks in MPI while another thread needs to run a collection. The flip side is that in a multi-threaded program the garbage collector can now run concurrently with a blocking MPI call, and its finalizers can free MPI handles while another thread is still inside MPI, which constitutes concurrent MPI calls from different threads and requires initializing MPI with `MPI_THREAD_MULTIPLE`. Add a section about this to the known issues page and recommend `threadlevel=:multiple` for multi-threaded programs in the docstring of `MPI.Init`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cdabf24 to
5bfda86
Compare
Note: the diff is quite large because the file with the generated API changed a lot. I split the changes into different logically consistent commits, for easier review. I also got the bot to always generate from now on the file with a stable sorting, instead of relying on the order of the functions in the upstream header file (which isn't really stable).
@vchuravy for the time being I marked only
MPI_WaitandMPI_Allreduce, do we need to support more functions?Fix #954.