Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ShellCheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ on:
- master
- release-*
tags: '*'
paths:
paths: &paths
- 'bin/**'
- 'gen/gen.sh'
pull_request:
paths:
- 'bin/**'
paths: *paths

concurrency:
# Skip intermediate builds: always.
Expand All @@ -26,4 +26,4 @@ jobs:
- name: Install dependencies
run: sudo apt install shellcheck
- name: Check scripts
run: shellcheck bin/*
run: shellcheck bin/* gen/gen.sh
57 changes: 56 additions & 1 deletion docs/src/reference/api.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,62 @@
# Low-level API

The `MPI.API` submodule provides a low-level interface which closely matches the MPI C API.
While these functions are not intended for general usage, they are useful for calling MPI routines not yet available in `MPI.jl` main interface, and is the basis for the high-level wrappers. The methods suffixed with `_c` allow `MPI_count` typed arguments (vs `int` for the standard ones). The size of `MPI_count` depends on the implementation, but usually allows `64bit` integer offsets.
While these functions are not intended for general usage, they are useful for calling MPI routines not yet available in `MPI.jl` main interface, and is the basis for the high-level wrappers.

## Large counts

The methods suffixed with `_c` are the MPI 4.0 large-count entry points: they take
`MPI_Count` typed arguments where the others take `int`. The size of `MPI_Count` depends
on the implementation, but usually allows 64-bit integer counts, so these are the methods
to use for messages of more than `typemax(Cint)` elements.

They can be called against any supported MPI library. Where the library does not provide
them, each `MPI_Foo_c` falls back to the narrow `MPI_Foo` at precompilation time. That is
all-or-nothing: an implementation may ship only part of the set -- Intel MPI 2021.11 has
`MPI_Send_c` but not `MPI_Type_size_c` -- and because the count types below are one choice
for the whole package, using the large-count entry points that happen to be present would
leave a `Ref{MPI_Count}` reaching an entry point that wants a `Ptr{Cint}`. A count that does not fit in an `int` then raises an `InexactError`
instead of being silently truncated. [`MPI.API.HAS_LARGE_COUNT`](@ref) says which of the
two is in use.

Note that having them does not by itself make messages of more than `typemax(Cint)`
elements possible. Displacements and datatype extents are `MPI_Aint`, which is
pointer-sized, so on a 32-bit build neither can describe more than 2 GiB however wide
`MPI_Count` is -- a 32-bit address space could not hold such a buffer anyway.

The fallback is transparent for a count passed by value, since `ccall` converts it, but
not for one passed through a pointer. Type arrays and `Ref`s with
[`MPI.API.Count`](@ref), [`MPI.API.Displ`](@ref), or [`MPI.API.TypeDispl`](@ref) need to
have the type corresponding to the entry point they use. For example, a `Ref{MPI_Count}`
reaching a fallback that expects a `Ptr{Cint}` is a `MethodError`. (In other words, the
fallback mechanism is always safe.)

Two functions taking callback arguments are deliberately excluded from that fallback because
their callback signatures differ between their non-large-count and large-count versions:
`MPI_Op_create_c` takes an `MPI_User_function_c`
(whose `len` argument is an `MPI_Count *`) rather than an `MPI_User_function` (an
`int *`), and `MPI_Register_datarep_c` likewise. Both raise
[`MPI.FeatureLevelError`](@ref) on a library that does not provide them. It is fine
to create an operator with a non-large-count version of these functions, and then call the
large-count version of a reduction operation.
(All MPI implementations we tested can automatically chunk large reduction counts when the
registered operator cannot handle them in one go.)

## Feature levels

Wrappers for procedures that the MPI library does not provide are replaced, when MPI.jl is
precompiled, by a `throw(`[`MPI.FeatureLevelError`](@ref)`)`. The reported minimum MPI
version comes from a table derived from the MPI standard's machine-readable binding
description; see `MPI.jl/gen/versions/README.md`. (Procedures that have been in MPI since
3.1 or earlier do not have such version information since MPI.jl requires MPI 3.0 or later anyway.)

```@docs
MPI.API.@mpichk
MPI.API.HAS_LARGE_COUNT
MPI.API.Count
MPI.API.Displ
MPI.API.TypeDispl
```

```@autodocs
Modules = [MPI.API]
Expand Down
13 changes: 10 additions & 3 deletions gen/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ MPIPreferences = "3da0fdf6-3ccc-4f1b-acd9-58baa6c99267"
OpenMPI_jll = "fe0851c0-eecd-5654-98d4-656369965a5c"

[compat]
Clang = "0.17.1"
MPIPreferences = "0.1.3"
julia = "1.6"
# Which MPI standard version the generated bindings cover is decided here: the
# headers come from these JLLs. MPICH 5.0 is MPI 5.0, Open MPI 5.0 is MPI 3.1.
MPICH_jll = "5"
OpenMPI_jll = "5"
MPIPreferences = "0.1.8"
# Clang 0.17 needs Clang_jll <= 15, hence an LLVM 15 Julia, hence Julia 1.10.
# Pinned to its minor version because Clang.jl formats the whole generated file:
# be careful with version bumps, don't mix them with changes of the MPI version.
Clang = "~0.17"
julia = "~1.10"
69 changes: 65 additions & 4 deletions gen/gen.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,66 @@
#!/usr/bin/env bash
{
${JULIA-julia} --project=@. -e 'import MPIgenerator; MPIgenerator.signatures()'
exit
}
#
# Regenerate ../src/api/generated_api.jl by running Clang.jl over an MPI
# implementation's headers. Run this by hand and commit the result.
# (Note: We could automate this in CI?)
#
# cd gen
# JULIA="julia +1.10" ./gen.sh # read MPICH's headers (the default)
# JULIA="julia +1.10" ./gen.sh OpenMPI_jll # read Open MPI's instead
#
# You need to use Julia 1.10. The pinned Clang.jl needs Clang_jll <= 15, which in turn
# needs the LLVM 15 that only this version of Julia uses. (You can use `juliaup add 1.10`
# to install it.) The shell variable JULIA may contain arguments, as in the juliaup example above.
# Leave this variable unset to use whatever `julia` is on your PATH.
# The first run downloads Clang_jll, which is over a gigabyte, so expect it to take a while.
#
# Which MPI standard version the bindings cover is decided by the JLL versions that
# gen/Project.toml pins. MPICH 5.0 provides MPI 5.0, Open MPI 5.0 provides only MPI 3.1. To move
# to a newer standard, raise the bound there and rerun.
# Note: Bumping the Clang bound changes the formatting of the whole output file, don't do this
# while also switching to a new MPI version.
#
# When reviewing the diff it is a good idea to look for:
#
# * signature changes to procedures that already existed -- const-correctness or an
# int -> MPI_Count change in a header would change the `ccall` argument type;
# * procedures whose C return value is not an error code. `@mpichk` would treat one as
# an errcode and throw on any nonzero result. Those are caught automatically from the
# `ccall` return type, so a new one shows up as `@mpicall`; a `Cint`-returning
# procedure that is not error-checked has to go in `mpicall` in src/MPIgenerator.jl;
# * names that collide with something hand-written in ../src/api/, which is what the
# ignorelist in src/generator.toml is for.
#
# A new MPI standard version also means refreshing src/versions.jl, which describes the
# minimum version of each procedure, and the set of functions which have large-count `MPI_*_c`
# entry points. They have their own script and their own instructions; see versions/README.md.

set -euo pipefail
cd "$(dirname "$0")"

binary=${1:-MPICH_jll}

# Pin which implementation's headers to read, rather than following whatever
# MPIPreferences the surrounding environment resolves to. This may not even be one
# that the generator supports. Written directly instead of through
# `MPIPreferences.use_jll_binary` because `Pkg.instantiate` below already consults
# this preference (MPICH_jll's platform augmentation reads it at resolve time), so it
# has to be in place before any Julia code can run.
cat >LocalPreferences.toml <<TOML
[MPIPreferences]
_format = "1.0"
binary = "$binary"
preloads = []
__clear__ = ["libmpi", "abi", "mpiexec", "cclibs", "preloads_env_switch"]
TOML

# Allow `JULIA` to contain arguments, e.g. a juliaup channel.
read -r -a julia <<<"${JULIA:-julia}"

# Start from a clean resolution every time. gen/Manifest.toml is not committed, so
# it is not part of what makes this reproducible. Reproducibility depends on the compat bounds
# in gen/Project.toml, and a stale Manifest.toml left over from an earlier run would
# otherwise be reused and could then generate with the wrong MPI version.
rm -f Manifest.toml
"${julia[@]}" --project=. -e 'using Pkg; Pkg.instantiate()'
"${julia[@]}" --project=. -e 'import MPIgenerator; MPIgenerator.signatures()'
105 changes: 83 additions & 22 deletions gen/src/MPIgenerator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ module MPIgenerator
using Clang.Generators
using MPIPreferences

# INTRODUCED: procedure => the MPI version that added it
# EMBIGGENED: procedures that have a large-count `MPI_*_c` entry point
# Both are derived from the MPI standard's own data; see gen/versions/README.md.
include("versions.jl")

# Note: We could, in principle, also use the MPI standard itself to generate these functions,
# instead of looking at a particular MPI library.
if MPIPreferences.binary == "MPICH_jll"
import MPICH_jll: artifact_dir
elseif MPIPreferences.binary == "OpenMPI_jll"
Expand All @@ -10,6 +17,27 @@ module MPIgenerator
error("Unknown MPI binary: $(MPIPreferences.binary)")
end

"""
embiggened_base(sym, line)

For a large-count entry point `MPI_Foo_c`, the narrow `MPI_Foo` to fall back on when
the MPI library does not provide it (see `@mpichk`'s `fallback=`), or `nothing`.

A function which takes a callback argument that is also widened is excluded: `MPI_Op_create_c` takes an
`MPI_User_function_c` (`MPI_Count *len`) while `MPI_Op_create` takes an
`MPI_User_function` (`int *len`), so handing one creator's callback to the other
would make it load the wrong width. Same for `MPI_Register_datarep_c`. Such a function is
detected by a `*_function_c` argument type rather than listed by name.
"""
function embiggened_base(sym, line)
name = string(sym)
endswith(name, "_c") || return nothing
base = Symbol(chop(name; tail=2))
base ∈ EMBIGGENED || return nothing
occursin(r"_function_c\}", line) && return nothing
return base
end

signatures() = begin
@info "Generate MPI bindings from $(MPIPreferences.binary)"

Expand All @@ -18,7 +46,12 @@ module MPIgenerator
mkpath(out)

options = load_options(joinpath(@__DIR__, "generator.toml")) # wrapper generator options
options["general"]["callback_documentation"] = node -> [string('$', "(_doc_external(:", node.id, "))")]
# Pass the introducing MPI version along: it decides whether Open MPI's released
# documentation covers the procedure, and so whether to link there at all.
options["general"]["callback_documentation"] = node -> begin
ver = get(INTRODUCED, Symbol(node.id), nothing)
[string('$', "(_doc_external(:", node.id, isnothing(ver) ? "" : ", $(repr(ver))", "))")]
end

include_dir = normpath(artifact_dir, "include")

Expand All @@ -39,39 +72,67 @@ module MPIgenerator
# custom MPI post-processing
rm(joinpath(out, "common.jl")) # remove un-needed file

# these methods must be called with `@mpicall` instead of `@mpichk`
# These methods must be called with `@mpicall` instead of `@mpichk`. They do
# return an error code, but they are called during precompilation, before MPI.jl can
# turn one into an `MPIError` (whose message comes from `MPI_Error_string`).
#
# Functions whose return value is not an error code at all are detected below
# from their `ccall` return type instead of being listed here -- `MPI_Wtime` and
# `MPI_Wtick` return a `Cdouble`, `MPI_Aint_add` and `MPI_Aint_diff` an
# `MPI_Aint`. We cannot error-check them.
mpicall = (
:MPI_Get_library_version,
:MPI_Get_processor_name,
:MPI_Get_version,
:MPI_Wtime,
:MPI_Wtick,
)

versioned = Dict(
:MPI_Dist_graph_create_adjacent => v"2.2",
:MPI_Dist_graph_neighbors_count => v"2.2",
:MPI_Dist_graph_neighbors => v"2.2",
:MPI_Dist_graph_create => v"2.2",
:MPI_Neighbor_allgatherv => v"3.0",
:MPI_Neighbor_alltoallv => v"3.0",
:MPI_Neighbor_allgather => v"3.0",
:MPI_Neighbor_alltoall => v"3.0",
)
# Repo-relative rather than derived from the absolute path: `r".*MPI.jl"` keeps
# everything after the last "MPI.jl" in the path, which in a git worktree is
# ".claude/worktrees/<name>/gen/src/...", leaking the checkout into the banner.
src = joinpath(out, "api.jl")
fn = "MPI.jl/" * replace(relpath(@__FILE__, normpath(@__DIR__, "..", "..")), '\\' => '/')
lines = String["# WARNING: this signature file for $(MPIPreferences.binary) has been auto-generated, please edit $fn instead!\n"]

# Collected below, then spliced in at the top: `@mpichk` reads HAS_LARGE_COUNT
# while expanding the wrappers in this file, so it has to be defined before them.
largecount = Symbol[]
body = String[]

src, fn = joinpath(out, "api.jl"), replace(@__FILE__, r".*MPI.jl" => "MPI.jl")
lines = String["# WARNING: this signature file for $(MPIPreferences.binary) has been auto-generated, please edit $fn instead !\n"]
for line in readlines(src)
if (m = match(r"^ccall.*:([\w_]+)", lstrip(line))) ≢ nothing
sym = first(m.captures) |> Symbol
repl = sym ∈ mpicall ? "@mpicall ccall" : "@mpichk ccall"
if (m = match(r"^ccall\(\(:([\w_]+), libmpi\), ([^,]+),", lstrip(line))) ≢ nothing
sym, returntype = Symbol(m.captures[1]), strip(m.captures[2])
errorchecked = returntype == "Cint" && sym ∉ mpicall
repl = errorchecked ? "@mpichk ccall" : "@mpicall ccall"
line = replace(line, "Ptr{Cvoid}" => "MPIPtr", "ccall" => repl)
if (ver = get(versioned, sym, nothing)) ≢ nothing
line *= " $(repr(ver))"
if errorchecked
if (ver = get(INTRODUCED, sym, nothing)) ≢ nothing
line *= " $(repr(ver))"
end
if (base = embiggened_base(sym, line)) ≢ nothing
line *= " fallback=$base"
push!(largecount, sym)
end
end
end
push!(lines, replace(line, raw"\$" => '$'))
push!(body, replace(line, raw"\$" => '$'))
end

# An implementation may provide only some of the large-count entry points: Intel
# MPI 2021.11 has `MPI_Send_c` but not `MPI_Type_size_c`. Since `API.Count` and
# its companions are one choice for the whole package, falling back per function
# would let a `Ref{MPI_Count}` reach an entry point wanting a `Ptr{Cint}`. So the
# fallback is all-or-nothing, decided here over every one of them.
append!(lines, [
"",
"const LARGE_COUNT_SYMBOLS = (",
join((" :$sym," for sym in largecount), "\n"),
")",
"",
"const HAS_LARGE_COUNT =",
" all(sym -> !isnothing(dlsym(libmpi_handle, sym; throw_error=false)), LARGE_COUNT_SYMBOLS)",
"",
])
append!(lines, body)
write(src, join(lines, "\n"))

dst = normpath(@__DIR__, "..", "..", "src", "api", "generated_api.jl")
Expand Down
27 changes: 26 additions & 1 deletion gen/src/generator.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,29 @@
library_name = "libmpi"
output_common_file_path = "./out/common.jl"
output_api_file_path = "./out/api.jl"
output_ignorelist = ["^[PQ]MPI[XR]?_.*", "^MPI[XR]_.*", "^MPI_T_.*", "^MPI_Session_.*", "^MPI_Status_[\\w_]*f08.*", "^MPI_Group_from_session_pset.*"]
output_ignorelist = [
# Deliberately outside MPI.jl's scope: the profiling and vendor-extension
# prefixes, the tool information interface, sessions, and the Fortran 2008
# status conversions.
"^[PQ]MPI[XR]?_.*",
"^MPI[XR]_.*",
"^MPI_T_.*",
"^MPI_Session_.*",
"^MPI_Status_[\\w_]*f08.*",
"^MPI_Group_from_session_pset.*",
# Handle <-> Fortran/integer conversions. These return a handle rather than an error
# code, so `@mpichk` would mistake the handle for an errcode; `MPI_Comm_f2c` and
# friends are instead written by hand in src/api/api.jl, which also handles ABIs
# which implement these as macros.
# `MPI_Status_f2c` and `MPI_Status_c2f` do return error codes, so they are deliberately not ignored.
"^MPI_(Comm|Datatype|Errhandler|File|Group|Info|Message|Op|Request|Type|Win)_(f2c|c2f|toint|fromint)$",
# Note: nothing needs to ignore `MPI_Pcontrol`. Clang.jl does not generate a wrapper
# for a variadic declaration at all, and it is the only one in the MPI C API; it is
# written by hand in src/api/api.jl instead.
#
# Predefined callbacks, not callable entry points. `MPI_DUP_FN` in particular is
# already a `@const_ref` in src/api/mpiabi.jl, so generating a function of that name
# is an invalid redefinition of a constant.
"^MPI_(DUP|NULL_COPY|NULL_DELETE)_FN$",
"^MPI_CONVERSION_FN_NULL.*",
]
Loading
Loading