Repository: carstenbauer/ThreadPinning.jl
Pull request #54 (2023-01-27 21:23:55)

Initial MPI support
See #51 


Patch:
diff --git a/Project.toml b/Project.toml
index 7be3db4..92fc427 100644
--- a/Project.toml
+++ b/Project.toml
@@ -5,7 +5,6 @@ version = "0.7.2"
 
 [deps]
 DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
-DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
 Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
 Preferences = "21216c6a-2e73-6563-6e65-726566657250"
@@ -13,7 +12,6 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c"
 
 [compat]
-DocStringExtensions = "0.9"
 MKL = "0.4, 0.6"
 Preferences = "1"
 SnoopPrecompile = "1"
diff --git a/docs/make.jl b/docs/make.jl
index a93758c..b6e7a6a 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -22,6 +22,8 @@ makedocs(;
              "ThreadPinning" => "index.md",
              "Examples" => [
                  "Pinning Julia Threads" => "examples/ex_pinning_julia_threads.md",
+                 "Respect Process Affinity Mask" => "examples/ex_affinity.md",
+                 "MPI and MPI + Threads" => "examples/ex_mpi.md",
                  "Autochecking BLAS Thread Settings" => "examples/ex_blas.md",
                  "Measuring Core-to-Core Latency" => "examples/ex_core2core_latency.md",
              ],
@@ -48,8 +50,8 @@ if ci
     @info "Deploying documentation to GitHub"
     deploydocs(;
                repo = "github.com/carstenbauer/ThreadPinning.jl.git",
-               devbranch = "main"
-               # push_preview=true,
+               devbranch = "main",
+               push_preview=true,
                # target = "site",
                )
 end
diff --git a/docs/src/examples/ex_affinity.md b/docs/src/examples/ex_affinity.md
new file mode 100644
index 0000000..1fc6150
--- /dev/null
+++ b/docs/src/examples/ex_affinity.md
@@ -0,0 +1,122 @@
+# [Process Affinity Mask](@id exaffinitymask)
+
+In scenarios where the Julia process has a specific affinity mask, e.g. when running under `taskset`, `numactl`, or (perhaps) SLURM, you may want to pin your Julia threads in accordance with this affinity mask. To that end, we provide [`pinthreads(:affinitymask)`](@ref), which pins Julia threads to non-masked CPU-threads (in order, hyperthreads are only used if necessary).
+
+For the demonstration below, we consider the following Julia script:
+```julia
+$ cat check.jl 
+using ThreadPinning
+if length(ARGS) > 0 && ARGS[1] == "pin"
+    pinthreads(:affinitymask)
+end
+println(getcpuids())
+println("no double occupancies: ", length(unique(getcpuids())) == length(getcpuids()))
+println("in order: ", issorted(getcpuids()))
+```
+
+## `taskset`
+
+Let's use `taskset --cpu-list` to set the affinity of the Julia process.
+
+```julia
+$ taskset --cpu-list 0-24 julia --project -t 25 check.jl
+[13, 13, 4, 5, 6, 7, 8, 11, 15, 14, 12, 16, 18, 19, 0, 10, 3, 9, 24, 2, 17, 20, 1, 21, 21]
+no double occupancies: false
+in order: false
+```
+
+Note that
+
+1) some Julia threads **may** run on the same CPU-thread(!) (which is almost certainly not desired), and
+2) the order of the Julia thread to CPU-thread mapping is arbitrary (and non-deterministic).
+
+We can remedy both points with `pinthreads(:affinitymask)`:
+
+```julia
+$ taskset --cpu-list 0-24 julia --project -t 25 check.jl pin
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+no double occupancies: true
+in order: true
+```
+
+## `numactl`
+
+The same comments as made for [`taskset`](@ref) above also apply to `numactl --physcpubind`. Without `pinthreads(:affinitymask)`:
+
+```julia
+$ numactl --physcpubind=0-24 julia --project -t 25 check.jl 
+[6, 10, 7, 13, 14, 15, 8, 16, 19, 0, 5, 2, 3, 4, 18, 1, 9, 17, 20, 20, 12, 20, 10, 20, 11]
+no double occupancies: false
+in order: false
+```
+
+With `pinthreads(:affinitymask)`:
+
+```julia
+$ numactl --physcpubind=0-24 julia --project -t 25 check.jl pin
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+no double occupancies: true
+in order: true
+```
+
+## SLURM
+
+!!! note
+    SLURM settings vary a lot between clusters, in particular affinity related settings. In the following, we visualize the affinity mask set by SLURM at the top of the output files (`B` means "this CPU can be used" whereas `-` indicates "this CPU can't be used" and vertical lines indicate different domains.). Be wary that the same job scripts might not set affinity masks on your cluster!
+
+```julia
+$ cat slurm_basic.jl 
+#!/usr/bin/env sh
+#SBATCH -N 1
+#SBATCH -n 1
+#SBATCH --cpus-per-task 25
+#SBATCH -o sl_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+
+srun -n 1 julia --project -t 25 check.jl 
+```
+
+Without `pinthreads(:affinitymask)`:
+```
+$ cat sl_2374255.out 
+cpu-bind=MASK - cn-0181, task  0  0 [1410285]: mask |BBBBBBBBBBBBBBBBBBBB||||BBBBB---------------|  set
+cpu-bind=MASK - cn-0181, task  0  0 [1410316]: mask |BBBBBBBBBBBBBBBBBBBB||||BBBBB---------------|  set
+[11, 16, 17, 2, 15, 18, 19, 13, 3, 4, 5, 6, 7, 10, 9, 8, 14, 11, 20, 0, 12, 13, 4, 2, 1]
+no double occupancies: false
+in order: false
+```
+
+Note that
+
+1) some Julia threads **may** run on the same CPU-thread(!) (which is almost certainly not desired), and
+2) the order of the Julia thread to CPU-thread mapping is arbitrary (and non-deterministic).
+
+We can remedy both points with `pinthreads(:affinitymask)`:
+```
+$ cat sl_2374256.out 
+cpu-bind=MASK - cn-0197, task  0  0 [1507377]: mask |BBBBBBBBBBBBBBBBBBBB||||BBBBB---------------|  set
+cpu-bind=MASK - cn-0197, task  0  0 [1507410]: mask |BBBBBBBBBBBBBBBBBBBB||||BBBBB---------------|  set
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+no double occupancies: true
+in order: true
+```
+
+## Supplement: `-tauto` (Julia >= 1.9)
+
+For Julia >= 1.9 you can use `-tauto` to automatically set the number of Julia threads such that it matches the external affinity mask ([relevant PR](https://github.com/JuliaLang/julia/pull/42340)). This is particularly useful when using SLURM, but, for simplicity, we can also showcase it with `taskset`:
+
+```
+$ taskset --cpu-list 0-24 julia +1.9 -tauto --project check.jl pin
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+no double occupancies: true
+in order: true
+```
+
+```
+$ taskset --cpu-list 0-11 julia +1.9 -tauto --project check.jl pin
+[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+no double occupancies: true
+in order: true
+```
\ No newline at end of file
diff --git a/docs/src/examples/ex_mpi.md b/docs/src/examples/ex_mpi.md
new file mode 100644
index 0000000..c31f467
--- /dev/null
+++ b/docs/src/examples/ex_mpi.md
@@ -0,0 +1,121 @@
+# MPI
+
+## SLURM
+
+In this section, we'll focus on MPI applications that run under SLURM (or a similar job scheduler). On most systems, the latter sets the affinity mask of the Julia processes (MPI ranks) based on the options set by the user (e.g. via `#SBATCH`). Consequently, one has to do little to nothing on the Julia side to achieve the desired pinning pattern.
+
+### MPI only
+
+If your MPI-parallel application is single threaded (i.e. one Julia thread per MPI rank), **you likely don't have to do anything on the Julia side** to pin the MPI ranks. Instead, you can just use the SLURM options.
+
+**Multinode example, 1 MPI rank per socket:**
+
+```julia
+#!/usr/bin/env sh
+#SBATCH -N 2 # two nodes
+#SBATCH -n 4 # four MPI ranks in total
+#SBATCH --ntasks-per-socket 1 # one MPI rank per socket
+#SBATCH -o sl_mpi_multinode_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+#=
+ml lang JuliaHPC # load Julia module (system specific!)
+srun -n 4 julia --project -t 1 $(scontrol show job $SLURM_JOBID | awk -F= '/Command=/{print $2}')
+exit
+# =#
+using MPI
+using ThreadPinning
+
+MPI.Init()
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+MPI.Barrier()
+sleep(2*rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+print_affinity_masks()
+```
+
+Output (manually cleaned up a bit):
+
+```
+Rank 0:
+    Host: n2fpga19
+    CPUs: [0]
+1:   |1000000000000000000000000000000000000000000000000000000000000000|0000000000000000000000000000000000000000000000000000000000000000|
+
+Rank 1:
+    Host: n2fpga19
+    CPUs: [64]
+1:   |0000000000000000000000000000000000000000000000000000000000000000|1000000000000000000000000000000000000000000000000000000000000000|
+
+Rank 2:
+    Host: n2fpga33
+    CPUs: [0]
+1:   |1000000000000000000000000000000000000000000000000000000000000000|0000000000000000000000000000000000000000000000000000000000000000|
+
+Rank 3:
+    Host: n2fpga33
+    CPUs: [64]
+1:   |0000000000000000000000000000000000000000000000000000000000000000|1000000000000000000000000000000000000000000000000000000000000000|
+```
+
+### Hybrid: MPI + Threads
+
+If your MPI-parallel application is multithreaded (i.e. multiple Julia threads per MPI rank), you can use [`pinthreads(:affinitymask)`](@ref) to pin Julia threads of each MPI rank according to the affinity mask set by SLURM (according to the user-specified options). If you don't use `pinthreads(:affinitymask)`, the Julia threads are only bound to a range of CPU-threads, they can migrate, and they can also overlap (occupy the same CPU-thread). See [Process Affinity Mask](@ref exaffinitymask) for more information.
+
+**Multinode example, 1 MPI rank per socket, 25 threads per rank:**
+
+```julia
+#!/usr/bin/env sh
+#SBATCH -N 2
+#SBATCH -n 4
+#SBATCH --ntasks-per-socket 1
+#SBATCH --cpus-per-task 25
+#SBATCH -o sl_hybrid_multinode_affinitymask_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+#=
+ml lang JuliaHPC
+srun -n 4 julia --project -t 25 $(scontrol show job $SLURM_JOBID | awk -F= '/Command=/{print $2}')
+exit
+# =#
+using MPI
+using ThreadPinning
+pinthreads(:affinitymask)
+
+MPI.Init()
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+MPI.Barrier()
+sleep(2*rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+```
+
+Output:
+
+```
+Rank 0:
+    Host: n2cn0853
+    CPUs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+Rank 1:
+    Host: n2cn0853
+    CPUs: [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88]
+Rank 2:
+    Host: n2cn0854
+    CPUs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+Rank 3:
+    Host: n2cn0854
+    CPUs: [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88]
+```
+
+## Manual
+
+In this section, we describe how you can pin the Julia threads of your MPI ranks manually, that is without any "help" from an external affinity mask (e.g. as set by SLURM, see above).
+
+TODO: [`pinthreads_mpi`](@ref)
\ No newline at end of file
diff --git a/docs/src/refs/api.md b/docs/src/refs/api.md
index e554d46..3748de2 100644
--- a/docs/src/refs/api.md
+++ b/docs/src/refs/api.md
@@ -10,7 +10,7 @@ Order   = [:function, :type]
 ## References - Pinning
 ```@autodocs
 Modules = [ThreadPinning]
-Pages   = ["pinning.jl"]
+Pages   = ["pinning.jl", "pinning_mpi.jl"]
 ```
 
 ## References - Querying
diff --git a/mpi/Manifest.toml b/mpi/Manifest.toml
new file mode 100644
index 0000000..363c6ea
--- /dev/null
+++ b/mpi/Manifest.toml
@@ -0,0 +1,238 @@
+# This file is machine-generated - editing it directly is not advised
+
+julia_version = "1.8.3"
+manifest_format = "2.0"
+project_hash = "fa6f87f1674be56bafd03db209b0b711d8e14a9f"
+
+[[deps.ArgTools]]
+uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
+version = "1.1.1"
+
+[[deps.Artifacts]]
+uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+
+[[deps.Base64]]
+uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+
+[[deps.CompilerSupportLibraries_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
+version = "0.5.2+0"
+
+[[deps.Dates]]
+deps = ["Printf"]
+uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
+
+[[deps.DelimitedFiles]]
+deps = ["Mmap"]
+uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"
+
+[[deps.Distributed]]
+deps = ["Random", "Serialization", "Sockets"]
+uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"
+
+[[deps.DocStringExtensions]]
+deps = ["LibGit2"]
+git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d"
+uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
+version = "0.9.3"
+
+[[deps.Downloads]]
+deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"]
+uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
+version = "1.6.0"
+
+[[deps.FileWatching]]
+uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
+
+[[deps.InteractiveUtils]]
+deps = ["Markdown"]
+uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
+
+[[deps.JLLWrappers]]
+deps = ["Preferences"]
+git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
+uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
+version = "1.4.1"
+
+[[deps.LazyArtifacts]]
+deps = ["Artifacts", "Pkg"]
+uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
+
+[[deps.LibCURL]]
+deps = ["LibCURL_jll", "MozillaCACerts_jll"]
+uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
+version = "0.6.3"
+
+[[deps.LibCURL_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
+uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
+version = "7.84.0+0"
+
+[[deps.LibGit2]]
+deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
+uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
+
+[[deps.LibSSH2_jll]]
+deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
+uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
+version = "1.10.2+0"
+
+[[deps.Libdl]]
+uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+
+[[deps.LinearAlgebra]]
+deps = ["Libdl", "libblastrampoline_jll"]
+uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
+
+[[deps.Logging]]
+uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
+
+[[deps.MPI]]
+deps = ["Distributed", "DocStringExtensions", "Libdl", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "Requires", "Serialization", "Sockets"]
+git-tree-sha1 = "6d72bafd3960f9c119ceb8f034fef28346490fe5"
+uuid = "da04e1cc-30fd-572f-bb4f-1f8673147195"
+version = "0.20.8"
+
+[[deps.MPICH_jll]]
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "Pkg", "TOML"]
+git-tree-sha1 = "6d4fa43afab4611d090b11617ecea1a144b21d35"
+uuid = "7cb0a576-ebde-5e09-9194-50597f1243b4"
+version = "4.0.2+5"
+
+[[deps.MPIPreferences]]
+deps = ["Libdl", "Preferences"]
+git-tree-sha1 = "71f937129731a29eabe6969db2c90368a4408933"
+uuid = "3da0fdf6-3ccc-4f1b-acd9-58baa6c99267"
+version = "0.1.7"
+
+[[deps.MPItrampoline_jll]]
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "Pkg", "TOML"]
+git-tree-sha1 = "b3f9e42685b4ad614eca0b44bd863cd41b1c86ea"
+uuid = "f1f71cc9-e9ae-5b93-9b94-4fe0e1ad3748"
+version = "5.0.2+1"
+
+[[deps.Markdown]]
+deps = ["Base64"]
+uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
+
+[[deps.MbedTLS_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
+version = "2.28.0+0"
+
+[[deps.MicrosoftMPI_jll]]
+deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
+git-tree-sha1 = "a16aa086d335ed7e0170c5265247db29172af2f9"
+uuid = "9237b28f-5490-5468-be7b-bb81f5f5e6cf"
+version = "10.1.3+2"
+
+[[deps.Mmap]]
+uuid = "a63ad114-7e13-5084-954f-fe012c677804"
+
+[[deps.MozillaCACerts_jll]]
+uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
+version = "2022.2.1"
+
+[[deps.NetworkOptions]]
+uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
+version = "1.2.0"
+
+[[deps.OpenBLAS_jll]]
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
+uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
+version = "0.3.20+0"
+
+[[deps.OpenMPI_jll]]
+deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "Pkg", "TOML"]
+git-tree-sha1 = "346d6b357a480300ed7854dbc70e746ac52e10fd"
+uuid = "fe0851c0-eecd-5654-98d4-656369965a5c"
+version = "4.1.3+3"
+
+[[deps.Pkg]]
+deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
+uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+version = "1.8.0"
+
+[[deps.Preferences]]
+deps = ["TOML"]
+git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d"
+uuid = "21216c6a-2e73-6563-6e65-726566657250"
+version = "1.3.0"
+
+[[deps.Printf]]
+deps = ["Unicode"]
+uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+
+[[deps.REPL]]
+deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
+uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
+
+[[deps.Random]]
+deps = ["SHA", "Serialization"]
+uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+
+[[deps.Requires]]
+deps = ["UUIDs"]
+git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
+uuid = "ae029012-a4dd-5104-9daa-d747884805df"
+version = "1.3.0"
+
+[[deps.SHA]]
+uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
+version = "0.7.0"
+
+[[deps.Serialization]]
+uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
+
+[[deps.SnoopPrecompile]]
+deps = ["Preferences"]
+git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c"
+uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c"
+version = "1.0.3"
+
+[[deps.Sockets]]
+uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
+
+[[deps.TOML]]
+deps = ["Dates"]
+uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
+version = "1.0.0"
+
+[[deps.Tar]]
+deps = ["ArgTools", "SHA"]
+uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
+version = "1.10.1"
+
+[[deps.ThreadPinning]]
+deps = ["DelimitedFiles", "Libdl", "LinearAlgebra", "Preferences", "Random", "SnoopPrecompile"]
+path = ".."
+uuid = "811555cd-349b-4f26-b7bc-1f208b848042"
+version = "0.7.2"
+
+[[deps.UUIDs]]
+deps = ["Random", "SHA"]
+uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+
+[[deps.Unicode]]
+uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+
+[[deps.Zlib_jll]]
+deps = ["Libdl"]
+uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
+version = "1.2.12+3"
+
+[[deps.libblastrampoline_jll]]
+deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
+uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"
+version = "5.1.1+0"
+
+[[deps.nghttp2_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
+version = "1.48.0+0"
+
+[[deps.p7zip_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
+version = "17.4.0+0"
diff --git a/mpi/Project.toml b/mpi/Project.toml
new file mode 100644
index 0000000..db71c19
--- /dev/null
+++ b/mpi/Project.toml
@@ -0,0 +1,3 @@
+[deps]
+MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195"
+ThreadPinning = "811555cd-349b-4f26-b7bc-1f208b848042"
diff --git a/mpi/hybrid_singlenode_pinthreadsmpi.jl b/mpi/hybrid_singlenode_pinthreadsmpi.jl
new file mode 100755
index 0000000..ad4f594
--- /dev/null
+++ b/mpi/hybrid_singlenode_pinthreadsmpi.jl
@@ -0,0 +1,23 @@
+#!/usr/bin/env sh
+#=
+ml lang JuliaHPC
+me=`basename "$0"`
+mpiexecjl -n 2 --oversubscribe --project julia -t 10 $me > $me.out
+exit
+# =#
+using MPI
+using ThreadPinning
+
+MPI.Init()
+
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+
+pinthreads_mpi(:sockets, rank, nranks)
+
+sleep(0.3 * rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+
+MPI.Finalize()
diff --git a/mpi/hybrid_singlenode_pinthreadsmpi.jl.out b/mpi/hybrid_singlenode_pinthreadsmpi.jl.out
new file mode 100644
index 0000000..ac88fca
--- /dev/null
+++ b/mpi/hybrid_singlenode_pinthreadsmpi.jl.out
@@ -0,0 +1,6 @@
+Rank 0:
+	Host: n2lcn0148
+	CPUs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+Rank 1:
+	Host: n2lcn0148
+	CPUs: [64, 65, 66, 67, 68, 69, 70, 71, 72, 73]
diff --git a/mpi/sl_hybrid_multinode_affinitymask_2905607.out b/mpi/sl_hybrid_multinode_affinitymask_2905607.out
new file mode 100644
index 0000000..6b3561f
--- /dev/null
+++ b/mpi/sl_hybrid_multinode_affinitymask_2905607.out
@@ -0,0 +1,17 @@
+cpu-bind=MASK - n2cn0853, task  0  0 [289653]: mask |BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------||||BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0853, task  0  0 [289718]: mask |BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0854, task  2  0 [270823]: mask |BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0853, task  1  1 [289719]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0854, task  3  1 [270824]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------|  set
+Rank 0:
+	Host: n2cn0853
+	CPUs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+Rank 1:
+	Host: n2cn0853
+	CPUs: [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88]
+Rank 2:
+	Host: n2cn0854
+	CPUs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
+Rank 3:
+	Host: n2cn0854
+	CPUs: [64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88]
diff --git a/mpi/sl_hybrid_multinode_naive_2905606.out b/mpi/sl_hybrid_multinode_naive_2905606.out
new file mode 100644
index 0000000..85970d8
--- /dev/null
+++ b/mpi/sl_hybrid_multinode_naive_2905606.out
@@ -0,0 +1,17 @@
+cpu-bind=MASK - n2cn0938, task  0  0 [2907650]: mask |BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------||||BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0938, task  0  0 [2907718]: mask |BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0938, task  1  1 [2907719]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0940, task  2  0 [2139173]: mask |BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0940, task  3  1 [2139174]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||BBBBBBBB|BBBBBBBB||BBBBBBBB|B-------||--------|--------||--------|--------|  set
+Rank 0:
+	Host: n2cn0938
+	CPUs: [4, 5, 6, 3, 7, 5, 11, 0, 4, 2, 11, 0, 1, 2, 13, 5, 3, 10, 8, 9, 15, 12, 13, 14, 11]
+Rank 1:
+	Host: n2cn0938
+	CPUs: [76, 66, 77, 78, 79, 72, 73, 71, 74, 64, 67, 65, 78, 73, 76, 66, 74, 75, 78, 70, 66, 73, 68, 74, 69]
+Rank 2:
+	Host: n2cn0940
+	CPUs: [13, 6, 2, 12, 14, 10, 8, 15, 0, 10, 1, 9, 12, 7, 5, 12, 4, 11, 3, 8, 16, 12, 8, 11, 11]
+Rank 3:
+	Host: n2cn0940
+	CPUs: [68, 76, 79, 77, 75, 73, 66, 70, 64, 73, 71, 65, 67, 73, 69, 73, 68, 75, 67, 74, 72, 78, 71, 73, 76]
diff --git a/mpi/sl_mpi_multinode_3045454.out b/mpi/sl_mpi_multinode_3045454.out
new file mode 100644
index 0000000..5e635e7
--- /dev/null
+++ b/mpi/sl_mpi_multinode_3045454.out
@@ -0,0 +1,21 @@
+cpu-bind=MASK - n2fpga19, task  0  0 [1654519]: mask |B-------|--------||--------|--------||--------|--------||--------|--------||||B-------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2fpga19, task  0  0 [1654582]: mask |B-------|--------||--------|--------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2fpga33, task  2  0 [174092]: mask |B-------|--------||--------|--------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2fpga19, task  1  1 [1654583]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||B-------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2fpga33, task  3  1 [174093]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||B-------|--------||--------|--------||--------|--------||--------|--------|  set
+Rank 0:
+	Host: n2fpga19
+	CPUs: [0]
+1:   |1000000000000000000000000000000000000000000000000000000000000000|0000000000000000000000000000000000000000000000000000000000000000|
+Rank 1:
+	Host: n2fpga19
+	CPUs: [64]
+Rank 2:
+1:   |0000000000000000000000000000000000000000000000000000000000000000|1000000000000000000000000000000000000000000000000000000000000000|
+	Host: n2fpga33
+	CPUs: [0]
+Rank 3:
+1:   |1000000000000000000000000000000000000000000000000000000000000000|0000000000000000000000000000000000000000000000000000000000000000|
+	Host: n2fpga33
+	CPUs: [64]
+1:   |0000000000000000000000000000000000000000000000000000000000000000|1000000000000000000000000000000000000000000000000000000000000000|
diff --git a/mpi/sl_mpi_singlenode_2905603.out b/mpi/sl_mpi_singlenode_2905603.out
new file mode 100644
index 0000000..be3b9bb
--- /dev/null
+++ b/mpi/sl_mpi_singlenode_2905603.out
@@ -0,0 +1,9 @@
+cpu-bind=MASK - n2cn0192, task  0  0 [3377960]: mask |B-------|--------||--------|--------||--------|--------||--------|--------||||B-------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0192, task  0  0 [3378022]: mask |B-------|--------||--------|--------||--------|--------||--------|--------||||--------|--------||--------|--------||--------|--------||--------|--------|  set
+cpu-bind=MASK - n2cn0192, task  1  1 [3378023]: mask |--------|--------||--------|--------||--------|--------||--------|--------||||B-------|--------||--------|--------||--------|--------||--------|--------|  set
+Rank 0:
+	Host: n2cn0192
+	CPUs: [0]
+Rank 1:
+	Host: n2cn0192
+	CPUs: [64]
diff --git a/mpi/slurm_hybrid_multinode_affinitymask.jl b/mpi/slurm_hybrid_multinode_affinitymask.jl
new file mode 100644
index 0000000..3e59399
--- /dev/null
+++ b/mpi/slurm_hybrid_multinode_affinitymask.jl
@@ -0,0 +1,30 @@
+#!/usr/bin/env sh
+#SBATCH -N 2
+#SBATCH -n 4
+#SBATCH --ntasks-per-socket 1
+#SBATCH --cpus-per-task 25
+#SBATCH -o sl_hybrid_multinode_affinitymask_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+#=
+ml lang JuliaHPC
+srun -n 4 julia --project -t 25 $(scontrol show job $SLURM_JOBID | awk -F= '/Command=/{print $2}')
+exit
+# =#
+using MPI
+using ThreadPinning
+
+MPI.Init()
+
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+
+pinthreads(:affinitymask)
+
+sleep(0.3 * rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+
+MPI.Finalize()
diff --git a/mpi/slurm_hybrid_multinode_naive.jl b/mpi/slurm_hybrid_multinode_naive.jl
new file mode 100644
index 0000000..5fb7eb9
--- /dev/null
+++ b/mpi/slurm_hybrid_multinode_naive.jl
@@ -0,0 +1,28 @@
+#!/usr/bin/env sh
+#SBATCH -N 2
+#SBATCH -n 4
+#SBATCH --ntasks-per-socket 1
+#SBATCH --cpus-per-task 25
+#SBATCH -o sl_hybrid_multinode_naive_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+#=
+ml lang JuliaHPC
+srun -n 4 julia --project -t 25 $(scontrol show job $SLURM_JOBID | awk -F= '/Command=/{print $2}')
+exit
+# =#
+using MPI
+using ThreadPinning
+
+MPI.Init()
+
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+
+sleep(0.3 * rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+
+MPI.Finalize()
diff --git a/mpi/slurm_mpi_multinode.jl b/mpi/slurm_mpi_multinode.jl
new file mode 100644
index 0000000..1f1e3d3
--- /dev/null
+++ b/mpi/slurm_mpi_multinode.jl
@@ -0,0 +1,28 @@
+#!/usr/bin/env sh
+#SBATCH -N 2
+#SBATCH -n 4
+#SBATCH --ntasks-per-socket 1
+#SBATCH -o sl_mpi_multinode_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+#=
+ml lang JuliaHPC
+srun -n 4 julia --project -t 1 $(scontrol show job $SLURM_JOBID | awk -F= '/Command=/{print $2}')
+exit
+# =#
+using MPI
+using ThreadPinning
+
+MPI.Init()
+
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+
+sleep(0.3 * rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+print_affinity_masks()
+
+MPI.Finalize()
diff --git a/mpi/slurm_mpi_singlenode.jl b/mpi/slurm_mpi_singlenode.jl
new file mode 100644
index 0000000..68a050d
--- /dev/null
+++ b/mpi/slurm_mpi_singlenode.jl
@@ -0,0 +1,27 @@
+#!/usr/bin/env sh
+#SBATCH -N 1
+#SBATCH -n 2
+#SBATCH --ntasks-per-socket 1
+#SBATCH -o sl_mpi_singlenode_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+#=
+ml lang JuliaHPC
+srun -n 2 julia --project -t 1 $(scontrol show job $SLURM_JOBID | awk -F= '/Command=/{print $2}')
+exit
+# =#
+using MPI
+using ThreadPinning
+
+MPI.Init()
+
+nranks = MPI.Comm_size(MPI.COMM_WORLD)
+rank = MPI.Comm_rank(MPI.COMM_WORLD)
+
+sleep(0.3 * rank)
+println("Rank $rank:")
+println("\tHost: ", gethostname())
+println("\tCPUs: ", getcpuids())
+
+MPI.Finalize()
diff --git a/slurm/check.jl b/slurm/check.jl
new file mode 100644
index 0000000..e8f3f9b
--- /dev/null
+++ b/slurm/check.jl
@@ -0,0 +1,7 @@
+using ThreadPinning
+if length(ARGS) > 0 && ARGS[1] == "pin"
+    pinthreads(:affinitymask)
+end
+println(getcpuids())
+println("no double occupancies: ", length(unique(getcpuids())) == length(getcpuids()))
+println("in order: ", issorted(getcpuids()))
diff --git a/slurm/slurm_basic.jl b/slurm/slurm_basic.jl
new file mode 100644
index 0000000..d01965c
--- /dev/null
+++ b/slurm/slurm_basic.jl
@@ -0,0 +1,10 @@
+#!/usr/bin/env sh
+#SBATCH -N 1
+#SBATCH -n 1
+#SBATCH --cpus-per-task 25
+#SBATCH -o sl_%j.out
+#SBATCH -A pc2-mitarbeiter
+#SBATCH -p all
+#SBATCH -t 00:02:00
+
+srun -n 1 julia --project -t 25 check.jl pin
diff --git a/src/ThreadPinning.jl b/src/ThreadPinning.jl
index ef4b752..b752f93 100644
--- a/src/ThreadPinning.jl
+++ b/src/ThreadPinning.jl
@@ -6,7 +6,6 @@ using Libdl
 using LinearAlgebra
 import Random
 using DelimitedFiles
-using DocStringExtensions
 
 # includes
 include("utility.jl")
@@ -17,6 +16,7 @@ include("libs/libuv.jl")
 include("libs/libpthread.jl")
 include("querying.jl")
 include("pinning.jl")
+include("pinning_mpi.jl")
 include("likwid-pin.jl")
 include("mkl.jl")
 include("openblas.jl")
@@ -126,12 +126,14 @@ end end
 export threadinfo,
        pinthreads,
        pinthreads_likwidpin,
+       pinthreads_mpi,
        pinthread,
        getcpuids,
        getcpuid,
        unpinthreads,
        unpinthread,
        @tspawnat,
+       print_affinity_masks,
        ncputhreads,
        ncores,
        nnuma,
diff --git a/src/libs/libuv.jl b/src/libs/libuv.jl
index 3e035aa..38c1e62 100644
--- a/src/libs/libuv.jl
+++ b/src/libs/libuv.jl
@@ -17,7 +17,7 @@ Ref: [docs](https://github.com/clibs/uv/blob/d0240ce496fcd86d45e8a6b211732220fdb
 uv_thread_self() = @ccall uv_thread_self()::uv_thread_t
 
 """
-$(SIGNATURES)
+    uv_thread_getaffinity(self_ref, cpumask, masksize)
 Gets the specified thread's affinity setting. On Unix, this maps the
 `cpu_set_t` returned by `pthread_getaffinity_np(3)` to bytes in `cpumask`.
 
@@ -34,7 +34,7 @@ function uv_thread_getaffinity(self_ref, cpumask, masksize)
 end
 
 """
-$(SIGNATURES)
+    uv_thread_getaffinity()
 Query the calling thread's affinity.
 """
 function uv_thread_getaffinity()
@@ -55,7 +55,7 @@ function uv_thread_getaffinity(threadid)
 end
 
 """
-$(SIGNATURES)
+    uv_thread_setaffinity(self_ref, cpumask, oldmask, masksize)
 Sets the specified thread's affinity to `cpumask`, which is specified in
 bytes. Optionally returning the previous affinity setting in `oldmask`.
 On Unix, uses `pthread_getaffinity_np(3)` to get the affinity setting
@@ -79,7 +79,7 @@ function uv_thread_setaffinity(self_ref, cpumask, oldmask, masksize)
 end
 
 """
-$(SIGNATURES)
+    uv_thread_setaffinity(procid::Integer)
 Set the calling thread's affinity to `procid`.
 """
 function uv_thread_setaffinity(procid::Integer)
diff --git a/src/likwid-pin.jl b/src/likwid-pin.jl
index f5577c0..4f2e353 100644
--- a/src/likwid-pin.jl
+++ b/src/likwid-pin.jl
@@ -1,5 +1,4 @@
 """
-$(SIGNATURES)
 Pins Julia threads to CPU-threads based on the given `likwid-pin` compatible string.
 Checkout the [LIKWID documentation](https://github.com/RRZE-HPC/likwid/wiki/Likwid-Pin)
 for more information.
@@ -21,7 +20,6 @@ function pinthreads_likwidpin(str::AbstractString; onebased = false)
 end
 
 """
-$(SIGNATURES)
 Convert the given likwid-pin compatible string into a CPU ID list.
 See [`pinthreads_likwidpin`](@ref) for more information.
 """
@@ -112,7 +110,6 @@ function _lp_check_domain(domain; onebased = false)
 end
 
 """
-$(SIGNATURES)
 The likwid-pin compatible domains that are available for the system.
 """
 function likwidpin_domains(; onebased = false)
diff --git a/src/mkl.jl b/src/mkl.jl
index 4818cc0..683d37c 100644
--- a/src/mkl.jl
+++ b/src/mkl.jl
@@ -12,7 +12,6 @@ function mkl_is_loaded()
 end
 
 """
-$(SIGNATURES)
 Returns the full path to the `libmkl_rt` library if the latter is loaded. Will try to
 locate the library and, if successfull, will cache the result. Throws an error otherwise.
 
@@ -52,7 +51,7 @@ Wrapper around the MKL function [`mkl_get_dynamic`](https://www.intel.com/conten
 mkl_get_dynamic() = @ccall mkl_fullpath().mkl_get_dynamic()::Cint
 
 """
-$(SIGNATURES)
+    mkl_set_dynamic(flag::Integer)
 Wrapper around the MKL function [`mkl_set_dynamic`](https://www.intel.com/content/www/us/en/develop/documentation/onemkl-developer-reference-c/top/support-functions/threading-control/mkl-set-dynamic.html).
 """
 function mkl_set_dynamic(flag::Integer)
diff --git a/src/openblas.jl b/src/openblas.jl
index fd6d9ba..bb35fb5 100644
--- a/src/openblas.jl
+++ b/src/openblas.jl
@@ -30,7 +30,6 @@ function _openblas_pinthread(threadid, cpuid; juliathread = Threads.threadid())
 end
 
 """
-$(SIGNATURES)
 Pin the available OpenBLAS threads to the given CPU IDs. Currently, only explict pinning
 is possible.
 """
@@ -57,7 +56,6 @@ Print the affinity masks of all OpenBLAS threads.
 function openblas_print_affinity_masks end
 
 """
-$(SIGNATURES)
 Returns the ID of the CPU thread on which the `i`-th OpenBLAS thread is currently
 running.
 
@@ -67,7 +65,6 @@ running.
 function openblas_getcpuid end
 
 """
-$(SIGNATURES)
 Returns the IDs of the CPU-threads on which the OpenBLAS threads are currently running.
 
 !!! note
diff --git a/src/pinning.jl b/src/pinning.jl
index d78c327..ff96b17 100644
--- a/src/pinning.jl
+++ b/src/pinning.jl
@@ -1,6 +1,7 @@
 """
-$(TYPEDSIGNATURES)
-Pin the calling Julia thread to the given CPU-thread.
+    pinthread(cpuid::Integer; warn::Bool = true)
+
+Pin the calling Julia thread to the CPU with id `cpuid`.
 """
 function pinthread(cpuid::Integer; warn::Bool = true)
     warn && _check_environment()
@@ -13,8 +14,9 @@ function pinthread(cpuid::Integer; warn::Bool = true)
 end
 
 """
-$(TYPEDSIGNATURES)
-Pin a Julia thread to a specific CPU-thread.
+    pinthread(threadid::Integer, cpuid::Integer; kwargs...)
+
+Pin the given Julia thread (`threadid`) to the CPU with ID `cpuid`.
 """
 function pinthread(threadid::Integer, cpuid::Integer; kwargs...)
     fetch(@tspawnat threadid pinthread(cpuid; kwargs...))
@@ -57,6 +59,9 @@ Simply provide an `AbstractVector{<:Integer}` of CPU IDs. The latter are expecte
 * `:random`: pin threads randomly to CPU-threads
 * `:current`: pin threads to the CPU-threads they are currently running on
 * `:firstn`: pin threads to CPU-threads in "physical" order (as specified by lscpu).
+* `:affinitymask`: pin threads to different CPU-threads in accordance with their
+                   affinity mask (must be the same for all of them). By default,
+                   `hyperthreads_last=true`.
 
 **3) Logical Specification**
 
@@ -125,6 +130,31 @@ end
 pinthreads(::Val{:random}; kwargs...) = pinthreads(node(; shuffle = true); kwargs...)
 pinthreads(::Val{:firstn}; kwargs...) = pinthreads(cpuids_all(); kwargs...)
 pinthreads(::Val{:current}; kwargs...) = pinthreads(getcpuids(); kwargs...)
+function pinthreads(::Val{:affinitymask}; hyperthreads_last = true,
+                    nthreads = Threads.nthreads(), kwargs...)
+    masks = get_affinity_mask.(1:nthreads)
+    mask = first(masks)
+    if !all(isequal(mask), masks)
+        error("Julia threads have different affinity masks.")
+    end
+    cpuids = get_cpuids_from_affinity_mask(mask)
+    if length(cpuids) < nthreads
+        error("More Julia threads than CPU-threads specified by affinity mask.")
+    end
+    if hyperthreads_last
+        # sort cpuids such that hyperthreads come last
+        by_func(c) = (c, ishyperthread(c))
+        lt_func(x, y) =
+            if x[2] != y[2]
+                return x[2] < y[2] # non-hyperthreads first
+            else
+                return x[1] < y[1] # lower cpuid first
+            end
+        sort!(cpuids; lt = lt_func, by = by_func)
+    end
+    pinthreads(cpuids; nthreads, kwargs...)
+    return nothing
+end
 
 """
 Unpins all Julia threads by setting the affinity mask of all threads to all unity.
@@ -141,8 +171,7 @@ function unpinthreads()
 end
 
 """
-$(SIGNATURES)
-Unpins the given Julia thread by setting the affinity mask to all unity.
+Unpins the Julia thread with the given `threadid` by setting the affinity mask to all unity.
 Afterwards, the OS is free to move the Julia thread from one CPU thread to another.
 """
 function unpinthread(threadid::Integer)
diff --git a/src/pinning_mpi.jl b/src/pinning_mpi.jl
new file mode 100644
index 0000000..f694ae2
--- /dev/null
+++ b/src/pinning_mpi.jl
@@ -0,0 +1,62 @@
+"""
+    pinthreads_mpi(symbol, rank, nranks; nthreads_per_rank, compact, kwargs...)
+
+Pin MPI ranks, that is, their respective Julia thread(s), to (subsets of) domains
+(e.g. sockets or memory domains). Specifically, when calling this function on all
+MPI ranks, the latter will be distributed in a round-robin fashion among the specified
+domains such that their Julia threads are pinned to non-overlapping ranges of CPU-threads
+within the domain.
+
+Valid options for `symbol` are `:sockets` and `:numa`.
+
+If `compact=false` (default), physical cores are occupied before hyperthreads. Otherwise,
+CPU-cores - with potentially multiple CPU-threads - are filled up one after another
+(compact pinning).
+
+The keyword argument `nthreads_per_rank` (default `Threads.nthreads()`) can be used to
+pin only a subset of the available Julia threads per MPI rank.
+
+**Note:**
+As per usual for MPI, `rank` starts at zero.
+
+*Example:*
+
+```
+using ThreadPinning
+using MPI
+comm = MPI.COMM_WORLD
+nranks = MPI.Comm_size(comm)
+rank = MPI.Comm_rank(comm)
+pinthreads_mpi(:sockets, rank, nranks)
+```
+"""
+function pinthreads_mpi(symb::Symbol, args...; kwargs...)
+    pinthreads_mpi(Val(symb), args...; kwargs...)
+end
+
+function pinthreads_mpi(::Val{:sockets}, rank::Integer, nranks::Integer;
+                        nthreads_per_rank = Threads.nthreads(),
+                        compact = false,
+                        kwargs...)
+    idx_in_socket, socketidx = divrem(rank, nsockets()) .+ 1
+    idcs = ((idx_in_socket - 1) * nthreads_per_rank + 1):(idx_in_socket * nthreads_per_rank)
+    if maximum(idcs) >= ncputhreads_per_socket()[socketidx]
+        error("Too many Julia threads / MPI ranks per socket.")
+    end
+    cpuids = socket(socketidx, idcs; compact)
+    pinthreads(cpuids; nthreads = nthreads_per_rank, kwargs...)
+    return nothing
+end
+function pinthreads_mpi(::Val{:numa}, rank::Integer, nranks::Integer;
+                        nthreads_per_rank = Threads.nthreads(),
+                        compact = false,
+                        kwargs...)
+    idx_in_numa, numaidx = divrem(rank, nnuma()) .+ 1
+    idcs = ((idx_in_numa - 1) * nthreads_per_rank + 1):(idx_in_numa * nthreads_per_rank)
+    if maximum(idcs) >= ncputhreads_per_numa()[numaidx]
+        error("Too many Julia threads / MPI ranks per memory domain (NUMA).")
+    end
+    cpuids = numa(numaidx, idcs; compact)
+    pinthreads(cpuids; nthreads = nthreads_per_rank, kwargs...)
+    return nothing
+end
diff --git a/src/preferences.jl b/src/preferences.jl
index 4ed0029..40b09cb 100644
--- a/src/preferences.jl
+++ b/src/preferences.jl
@@ -1,7 +1,6 @@
 module Prefs
 
 using Preferences
-using DocStringExtensions
 
 const ALL_PREFERENCES = ("autoupdate", "pin", "likwidpin")
 
@@ -23,13 +22,13 @@ function get_likwidpin()
     return p
 end
 
-"$(SIGNATURES)Set the pin preference"
+"Set the pin preference"
 function set_pin(s::Union{Symbol, AbstractString})
     # TODO check if valid?
     @set_preferences!("pin"=>String(s))
     return nothing
 end
-"$(SIGNATURES)Set the likwidpin preference"
+"Set the likwidpin preference"
 function set_likwidpin(s::AbstractString)
     # TODO check if valid?
     @set_preferences!("likwidpin"=>s)
@@ -68,7 +67,7 @@ function get_autoupdate()
     end
 end
 
-"$(SIGNATURES)Set the autoupdate preference"
+"Set the autoupdate preference"
 function set_autoupdate(b::Bool)
     @set_preferences!("autoupdate"=>string(b))
     @info("Done. Package might recompile next time it is loaded (in a new Julia session).")
diff --git a/src/querying.jl b/src/querying.jl
index 7e50b88..8e440eb 100644
--- a/src/querying.jl
+++ b/src/querying.jl
@@ -6,7 +6,6 @@ See `sched_getcpu` for more information.
 getcpuid() = Int(sched_getcpu())
 
 """
-$(SIGNATURES)
 Returns the ID of the CPU thread on which the given Julia thread (`threadid`) is currently
 running.
 """
@@ -54,6 +53,16 @@ function _affinity_mask_to_string(mask; groupby = :sockets)
     return str
 end
 
+"""
+Get the affinity mask of the given Julia Threads
+"""
+get_affinity_mask(tid) = uv_thread_getaffinity(tid)[1:ncputhreads()]
+
+"Get the CPU-thread IDs associated with the given affinity mask."
+function get_cpuids_from_affinity_mask(mask)
+    [unsafe_cpuids_all()[i] for (i, v) in enumerate(mask) if v == 1]
+end
+
 # Unsafe because they directly return the fields instead of copies (be warry when modiying!)
 unsafe_cpuids_all() = sysinfo().cpuids
 unsafe_cpuids_per_core() = sysinfo().cpuids_cores
@@ -63,7 +72,7 @@ unsafe_cpuids_per_node() = sysinfo().cpuids_node
 
 "Check whether hyperthreading is enabled."
 hyperthreading_is_enabled() = sysinfo().hyperthreading
-"$(SIGNATURES)Check whether the given CPU-thread is a hyperthread (i.e. the second CPU-thread associated
+"Check whether the given CPU-thread is a hyperthread (i.e. the second CPU-thread associated
 with a CPU-core)."
 ishyperthread(cpuid::Integer) = sysinfo().ishyperthread[_cpuidx(cpuid)]
 "Number of CPU-threads"
@@ -94,7 +103,6 @@ cpuids_all() = deepcopy(unsafe_cpuids_all())
 physical cores"
 cpuids_per_core() = deepcopy(unsafe_cpuids_per_core())
 """
-$(SIGNATURES)
 Returns a `Vector{Vector{Int}}` which indicates the CPUIDs associated with the available
 NUMA domains. Within each memory domain, physical cores come first. Set `compact=true` to
 get compact ordering instead.
@@ -108,7 +116,6 @@ function cpuids_per_numa(; compact = false)
     end
 end
 """
-$(SIGNATURES)
 Returns a `Vector{Vector{Int}}` which indicates the CPUIDs associated with the available
 CPU sockets. Within each socket, physical cores come first. Set `compact=true` to get
 compact ordering instead.
@@ -122,7 +129,6 @@ function cpuids_per_socket(; compact = false)
     end
 end
 """
-$(SIGNATURES)
 Returns a `Vector{Int}` which indicates the CPUIDs associated with the available node.
 Physical cores come first. Set `compact=true` to get compact ordering.
 """
@@ -149,7 +155,6 @@ end
 # High-level API for direct usage with `pinthreads`
 const T_idcs = Union{Colon, AbstractVector{<:Integer}, Integer}
 """
-$(SIGNATURES)
 Represents the CPU ID domain of core `i` (logical index, starts at 1). Uses compact ordering
 by default. Set `shuffle=true` to randomize.
 
@@ -165,7 +170,6 @@ function core(i::Integer, idcs::T_idcs = Colon(); shuffle = false, kwargs...)
     return cpuids
 end
 """
-$(SIGNATURES)
 Represents the CPU ID domain of NUMA/memory domain `i` (logical index, starts at 1). By
 default, cores will be used first and hyperthreads will only be used if necessary. Provide
 `compact=true` to get compact ordering instead. Set `shuffle=true` to randomize.
@@ -182,7 +186,6 @@ function numa(i::Integer, idcs::T_idcs = Colon(); shuffle = false, kwargs...)
     return cpuids
 end
 """
-$(SIGNATURES)
 Represents the CPU ID domain of socket `i` (logical index, starts at 1). By default, cores
 will be used first and hyperthreads will only be used if necessary. Provide `compact=true`
 to get compact ordering instead. Set `shuffle=true` to randomize.
@@ -199,7 +202,6 @@ function socket(i::Integer, idcs::T_idcs = Colon(); shuffle = false, kwargs...)
     return cpuids
 end
 """
-$(SIGNATURES)
 Represents the CPU ID domain of the entire node/system. By default, cores will be used first
 and hyperthreads will only be used if necessary. Provide `compact=true` to get compact
 ordering instead. Set `shuffle=true` to randomize. Set `shuffle=true` to randomize.
@@ -221,7 +223,6 @@ end
 #     return cpuids
 # end
 """
-$(SIGNATURES)
 Represents the CPU IDs of the system as obtained by a round-robin scattering
 between sockets. By default, within each socket, cores will be used first and hyperthreads
 will only be used if necessary. Provide `compact=true` to get compact ordering within each
@@ -241,7 +242,6 @@ function sockets(idcs::T_idcs = Colon(); shuffle = false, kwargs...)
     return cpuids
 end
 """
-$(SIGNATURES)
 Represents the CPU IDs of the system as obtained by a round-robin scattering
 between NUMA/memory domain. By default, within each memory domain, cores will be used first
 and hyperthreads will only be used if necessary. Provide `compact=true` to get compact
diff --git a/src/threadinfo.jl b/src/threadinfo.jl
index d2e43df..e559a96 100644
--- a/src/threadinfo.jl
+++ b/src/threadinfo.jl
@@ -1,5 +1,4 @@
 """
-$(SIGNATURES)
 Print information about Julia threads, e.g. on which CPU-threads (i.e. cores if
 hyperthreading is disabled) they are running.
 
diff --git a/src/utility.jl b/src/utility.jl
index ec85fc1..44af560 100644
--- a/src/utility.jl
+++ b/src/utility.jl
@@ -46,7 +46,6 @@ macro tspawnat(thrdid, expr)
 end
 
 """
-$(TYPEDSIGNATURES)
 # Examples
 ```julia
 interweave([1,2,3,4], [5,6,7,8]) == [1,5,2,6,3,7,4,8]


Test patch:
diff --git a/test/pinning_test.jl b/test/pinning_test.jl
index 778c72d..46d2d4d 100644
--- a/test/pinning_test.jl
+++ b/test/pinning_test.jl
@@ -1,7 +1,7 @@
 using Test
 using ThreadPinning
 using Base.Threads: nthreads
-using Random: shuffle
+using Random: shuffle, shuffle!
 
 Threads.nthreads() ≥ 2 ||
     error("Can't run tests with single Julia thread! Forgot to set `JULIA_NUM_THREADS`?")
@@ -72,6 +72,36 @@ end
         @test isnothing(pinthreads(:sockets; nthreads = 2))
         @test getcpuid.(1:2) == vcat(socket(1, 1:1), socket(2, 1:1))
     end
+    @testset ":affinitymask" begin
+        test_external_affinity = (cmd, numthreads, code) -> begin
+            julia = Base.julia_cmd()
+            pkgdir = joinpath(@__DIR__, "..")
+            juliacmd = `$julia --project=$(pkgdir) -t $numthreads -e $code`
+            fullcmd = `$cmd $juliacmd`
+            # @show fullcmd
+            run(fullcmd).exitcode == 0
+        end
+
+        numthreads = min(Threads.nthreads(), ncputhreads())
+        cpuids = sort!(shuffle!(cpuids_all())[1:numthreads])
+        cpulist = join(cpuids, ",")
+        # Check
+        # 1) all in mask
+        # 2) unique / no overlap
+        # 3) hyperthreads last
+        code = `"using ThreadPinning, Test;
+                 pinthreads(:affinitymask)
+                 mask_cpuids = [$cpulist]
+                 @test all(c->c in mask_cpuids, getcpuids())
+                 @test length(getcpuids()) == length(Set(getcpuids()))
+                 @test issorted(ishyperthread.(getcpuids()))"`
+        unpinthreads()
+        @testset "taskset" begin @test test_external_affinity(`taskset --cpu-list $cpulist`,
+                                                              numthreads, code) end
+        unpinthreads()
+        @testset "numactl" begin @test test_external_affinity(`numactl -C $cpulist`,
+                                                              numthreads, code) end
+    end
 end
 
 @testset "Thread Pinning (logical specification)" begin
@@ -137,7 +167,9 @@ end
     julia = Base.julia_cmd()
     pkgdir = joinpath(@__DIR__, "..")
 
-    exec(s; nthreads=ncores()) = run(`$julia --project=$(pkgdir) -t $nthreads -e $s`).exitcode == 0
+    function exec(s; nthreads = ncores())
+        run(`$julia --project=$(pkgdir) -t $nthreads -e $s`).exitcode == 0
+    end
     @testset "JULIA_PIN" begin
         withenv("JULIA_PIN" => "cputhreads") do
             @test exec(`'using ThreadPinning, Test;