Skip to content

Add gc_safe option to @mpicall and @mpichk macros - #955

Open
giordano wants to merge 14 commits into
JuliaParallel:masterfrom
giordano:mg/ccall-gc-safe
Open

Add gc_safe option to @mpicall and @mpichk macros#955
giordano wants to merge 14 commits into
JuliaParallel:masterfrom
giordano:mg/ccall-gc-safe

Conversation

@giordano

Copy link
Copy Markdown
Member

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_Wait and MPI_Allreduce, do we need to support more functions?

Fix #954.

giordano and others added 7 commits July 17, 2026 23:20
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>
@giordano
giordano requested a review from vchuravy July 17, 2026 23:07
giordano and others added 4 commits July 18, 2026 00:26
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>
Comment thread gen/src/MPIgenerator.jl
Comment on lines +55 to +58
gc_safe = (
:MPI_Wait,
:MPI_Allreduce,
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vchuravy for the time being I marked only MPI_Wait and MPI_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,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I am worried about is the MPI_Probe since they should be very fast.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bot really insisted that probes are fine

  • MPI_Mrecv — the one I nearly cut. After MPI_Mprobe the 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 inside MPI_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_Probe is one of the strongest candidates on the list — it sits at the opposite end of the spectrum from MPI_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_Probe precisely when it doesn't know when (or how big) the message will be. A rank sitting in MPI_Probe waiting 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 (unlike MPI_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.

giordano and others added 2 commits July 21, 2026 23:07
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>
@giordano
giordano requested a review from simonbyrne July 21, 2026 22:21
Comment thread docs/src/knownissues.md
Comment on lines +146 to +160
## 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use @ccall gc_safe=true for blocking calls

2 participants