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
17 changes: 17 additions & 0 deletions .buildkite/pipeline-julia.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ steps:
env:
CI_USE_OPENCL: "1"

# MPI × OpenCL SPMD datadeps suite. Runs on the same macOS/aarch64 agent as the
# non-MPI OpenCL job, but drives test/mpi_opencl.jl under MPI.jl's bundled
# `mpiexec` via test/run_mpi.jl (no `julia-test` plugin, no system MPI).
- label: Julia 1.11 (OpenCL, MPI)
<<: *gputest
timeout_in_minutes: 20
plugins:
- JuliaCI/julia#v1:
version: "1.11"
agents:
queue: "juliaecosystem"
os: macos
arch: aarch64
command: |
julia --project=test/openclenv -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=test/openclenv test/run_mpi.jl 2 2 test/mpi_opencl.jl

- label: Julia 1 - TimespanLogging
<<: *test
timeout_in_minutes: 20
Expand Down
55 changes: 55 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
timeout_in_minutes: 60
if: build.message !~ /\[skip tests\]/

# MPI × GPU jobs launch the SPMD datadeps suite (test/mpi_<backend>.jl) under
# `mpiexec`. They use only the `julia` plugin (not `julia-test`, which would run
# the non-MPI `test/runtests.jl`), then instantiate the backend's MPI test env
# and drive it with `test/run_mpi.jl`, which uses MPI.jl's bundled `mpiexec`
# (no system MPI required). 2 ranks share whatever GPU(s) the agent exposes;
# the data plane is host-staged P2P, so a single device is sufficient.
.mpigputest: &mpigputest
timeout_in_minutes: 60
if: build.message !~ /\[skip tests\]/

steps:
- label: Julia 1.11 (CUDA)
<<: *gputest
Expand Down Expand Up @@ -56,5 +66,50 @@ steps:
env:
CI_USE_METAL: "1"

- label: Julia 1.11 (CUDA, MPI)
<<: *mpigputest
plugins:
- JuliaCI/julia#v1:
version: "1.11"
agents:
queue: "cuda"
command: |
julia --project=test/cudaenv -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=test/cudaenv test/run_mpi.jl 2 2 test/mpi_cuda.jl

- label: Julia 1.11 (ROCm, MPI)
<<: *mpigputest
plugins:
- JuliaCI/julia#v1:
version: "1.11"
agents:
queue: "rocm"
command: |
julia --project=test/rocmenv -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=test/rocmenv test/run_mpi.jl 2 2 test/mpi_rocm.jl

- label: Julia 1.11 (oneAPI, MPI)
<<: *mpigputest
plugins:
- JuliaCI/julia#v1:
version: "1.11"
agents:
queue: "oneapi"
command: |
julia --project=test/oneapienv -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=test/oneapienv test/run_mpi.jl 2 2 test/mpi_oneapi.jl

- label: Julia 1.11 (Metal, MPI)
<<: *mpigputest
timeout_in_minutes: 20
plugins:
- JuliaCI/julia#v1:
version: "1.11"
agents:
queue: "metal"
command: |
julia --project=test/metalenv -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
julia --project=test/metalenv test/run_mpi.jl 2 2 test/mpi_metal.jl

# env:
# SECRET_CODECOV_TOKEN: ""
32 changes: 32 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,35 @@ jobs:
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}

mpi-cpu:
name: MPI CPU - Julia ${{ matrix.version }} / ${{ matrix.ranks }} ranks
runs-on: ubuntu-latest
timeout-minutes: 60
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
strategy:
fail-fast: false
matrix:
version:
- '1.10' # LTS
- '1.11'
- '1' # latest stable
ranks:
- 2
- 4
steps:
- uses: actions/checkout@v7
- uses: julia-actions/setup-julia@v3
with:
version: ${{ matrix.version }}
arch: x64
- uses: julia-actions/cache@v3
# test/mpienv pulls MPI (MPICH_jll by default) plus the checked-out Dagger;
# no system MPI installation is required. We `develop` the checkout rather
# than relying on the `[sources]` entry because `[sources]` is a Julia 1.11+
# feature and is silently ignored on 1.10 (LTS), which would otherwise
# resolve a stale registered Dagger lacking the MPI API (`accelerate!`).
- name: Instantiate CPU MPI environment
run: julia --project=test/mpienv -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
- name: Run CPU MPI tests
run: julia --project=test/mpienv test/run_mpi.jl ${{ matrix.ranks }} 2 test/mpi.jl
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
GraphViz = "f526b714-d49f-11e8-06ff-31ed36ee7ee0"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
LinuxPerf = "b4c46c6c-4fb0-484d-a11a-41bc3392d094"
MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195"
Metal = "dde4c033-4e86-420c-a63e-0dd931031962"
OpenCL = "08131aa3-fb12-5dee-8b74-c09406e224a2"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Expand All @@ -55,6 +56,7 @@ GraphVizSimpleExt = "Colors"
IntelExt = "oneAPI"
JSON3Ext = "JSON3"
LinuxPerfExt = "LinuxPerf"
MPIExt = "MPI"
MetalExt = "Metal"
OpenCLExt = "OpenCL"
PlotsExt = ["DataFrames", "Plots"]
Expand All @@ -78,6 +80,7 @@ Graphs = "1"
JSON3 = "1"
KernelAbstractions = "0.9"
LinuxPerf = "0.4.2"
MPI = "0.20.22"
MacroTools = "0.5"
MemPool = "0.4.17"
Metal = "1.1"
Expand Down
111 changes: 111 additions & 0 deletions contrib/mpi/check_comm_asymmetry.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env julia
# Parse MPI+Dagger logs and report communication decision asymmetry per tag.
# Asymmetry: for the same tag, one rank decides to send (local+bcast, sender+communicated, etc.)
# and another rank decides to infer (inferred, uninvolved) and never recv → deadlock.
#
# Usage: julia check_comm_asymmetry.jl < logfile
# Or: mpiexec -n 10 julia ... run_matmul.jl 2>&1 | tee matmul.log; julia check_comm_asymmetry.jl < matmul.log

const SEND_DECISIONS = Set([
"local+bcast", "sender+communicated", "sender+inferred", "receiver+bcast",
"aliasing", # when followed by local+bcast we already capture local+bcast
])
const RECV_DECISIONS = Set([
"communicated", "receiver", "sender+communicated", # received data
])
const INFER_DECISIONS = Set([
"inferred", "uninvolved", # did not recv (uses inferred type)
])

function parse_line(line)
# Match [rank X][tag Y] then any [...] and capture the last bracket pair before space or end
rank = nothing
tag = nothing
decision = nothing
category = nothing # aliasing, execute!, remotecall_endpoint
for m in eachmatch(r"\[rank\s+(\d+)\]", line)
rank = parse(Int, m.captures[1])
end
for m in eachmatch(r"\[tag\s+(\d+)\]", line)
tag = parse(Int, m.captures[1])
end
for m in eachmatch(r"\[(execute!|aliasing|remotecall_endpoint)\]", line)
category = m.captures[1]
end
# Decision is usually in last [...] that looks like [word] or [word+word]
for m in eachmatch(r"\]\[([^\]]+)\]", line)
candidate = m.captures[1]
# Normalize: "communicated" "inferred" "local+bcast" "sender+inferred" "receiver" etc.
if occursin("inferred", candidate) && !occursin("communicated", candidate)
decision = "inferred"
break
elseif occursin("communicated", candidate)
decision = "communicated"
break
elseif occursin("local+bcast", candidate)
decision = "local+bcast"
break
elseif occursin("sender+", candidate)
decision = startswith(candidate, "sender+inferred") ? "sender+inferred" : "sender+communicated"
break
elseif candidate == "receiver"
decision = "receiver"
break
elseif candidate == "receiver+bcast"
decision = "receiver+bcast"
break
elseif candidate == "inplace_move"
decision = "inplace_move"
break
end
end
return rank, tag, category, decision
end

function main()
# tag => Dict(rank => decision)
by_tag = Dict{Int, Dict{Int, String}}()
for line in eachline(stdin)
rank, tag, category, decision = parse_line(line)
isnothing(rank) && continue
isnothing(tag) && continue
isnothing(decision) && continue
if !haskey(by_tag, tag)
by_tag[tag] = Dict{Int, String}()
end
by_tag[tag][rank] = decision
end

# For each tag, check: is there at least one sender and one inferrer (non-receiver)?
send_keys = Set(["local+bcast", "sender+communicated", "sender+inferred", "receiver+bcast"])
infer_keys = Set(["inferred", "sender+inferred"]) # sender+inferred means sender didn't need to recv
recv_keys = Set(["communicated", "receiver", "sender+communicated"])

asymmetries = []
for (tag, ranks) in sort(collect(by_tag), by = first)
senders = [r for (r, d) in ranks if d in send_keys]
inferrers = [r for (r, d) in ranks if d in infer_keys || d == "uninvolved"]
receivers = [r for (r, d) in ranks if d in recv_keys]
# Asymmetry: someone sends (bcast) so will send to ALL other ranks; someone chose infer and won't recv.
if !isempty(senders) && !isempty(inferrers)
push!(asymmetries, (tag, senders, inferrers, receivers, ranks))
end
end

if isempty(asymmetries)
println("No communication decision asymmetry found (no tag has both sender and inferrer).")
return
end

println("=== Communication decision asymmetry (can cause deadlock) ===\n")
for (tag, senders, inferrers, receivers, ranks) in asymmetries
println("Tag $tag:")
println(" Senders (will bcast to all others): $senders")
println(" Inferrers (did not recv): $inferrers")
println(" Receivers: $receivers")
println(" All decisions: $ranks")
println()
end
end

main()
97 changes: 97 additions & 0 deletions contrib/mpi/check_comm_asymmetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""
Parse MPI+Dagger logs and report communication decision asymmetry per tag.
Asymmetry: for the same tag, one rank decides to send (local+bcast, etc.)
and another decides to infer (inferred) and never recv → deadlock.

Usage:
# Capture full log (all ranks' Core.println from mpi.jl go to stdout):
mpiexec -n 10 julia --project=/path/to/Dagger.jl benchmarks/run_matmul.jl 2>&1 | tee matmul.log
# Then look for asymmetry (same tag: one rank sends, another infers → deadlock):
python3 check_comm_asymmetry.py < matmul.log
"""

import re
import sys
from collections import defaultdict

SEND_DECISIONS = {"local+bcast", "sender+communicated", "sender+inferred", "receiver+bcast"}
RECV_DECISIONS = {"communicated", "receiver", "sender+communicated"}
INFER_DECISIONS = {"inferred", "uninvolved", "sender+inferred"}


def parse_line(line: str):
rank = tag = category = decision = None
m = re.search(r"\[rank\s+(\d+)\]", line)
if m:
rank = int(m.group(1))
m = re.search(r"\[tag\s+(\d+)\]", line)
if m:
tag = int(m.group(1))
m = re.search(r"\[(execute!|aliasing|remotecall_endpoint)\]", line)
if m:
category = m.group(1)
# Capture decision from [...] blocks
for m in re.finditer(r"\]\[([^\]]+)\]", line):
candidate = m.group(1)
if "inferred" in candidate and "communicated" not in candidate:
decision = "inferred"
break
if "communicated" in candidate:
decision = "communicated"
break
if "local+bcast" in candidate:
decision = "local+bcast"
break
if candidate.startswith("sender+"):
decision = "sender+inferred" if "inferred" in candidate else "sender+communicated"
break
if candidate == "receiver":
decision = "receiver"
break
if candidate == "receiver+bcast":
decision = "receiver+bcast"
break
if candidate == "inplace_move":
decision = "inplace_move"
break
return rank, tag, category, decision


def main():
by_tag = defaultdict(dict) # tag -> {rank: decision}
for line in sys.stdin:
rank, tag, category, decision = parse_line(line)
if rank is None or tag is None or decision is None:
continue
by_tag[tag][rank] = decision

send_keys = {"local+bcast", "sender+communicated", "sender+inferred", "receiver+bcast"}
infer_keys = {"inferred", "sender+inferred", "uninvolved"}
recv_keys = {"communicated", "receiver", "sender+communicated"}

asymmetries = []
for tag in sorted(by_tag.keys()):
ranks = by_tag[tag]
senders = [r for r, d in ranks.items() if d in send_keys]
inferrers = [r for r, d in ranks.items() if d in infer_keys]
receivers = [r for r, d in ranks.items() if d in recv_keys]
if senders and inferrers:
asymmetries.append((tag, senders, inferrers, receivers, ranks))

if not asymmetries:
print("No communication decision asymmetry found (no tag has both sender and inferrer).")
return

print("=== Communication decision asymmetry (can cause deadlock) ===\n")
for tag, senders, inferrers, receivers, ranks in asymmetries:
print(f"Tag {tag}:")
print(f" Senders (will bcast to all others): {senders}")
print(f" Inferrers (did not recv): {inferrers}")
print(f" Receivers: {receivers}")
print(f" All decisions: {dict(ranks)}")
print()


if __name__ == "__main__":
main()
Loading
Loading