Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
87112af
Add rocSHMEM as an allocation provider for Iris device kernels
nirvedhmeshram Sep 8, 2026
14a5fd9
Materialise each allocation's peer table from cached per-peer offsets
nirvedhmeshram Sep 8, 2026
e3c2955
Document collective semantics, table lifetime, and packaging
nirvedhmeshram Sep 11, 2026
2c709f7
Install rocSHMEM in the CI images so the provider tests run
nirvedhmeshram Sep 11, 2026
8daf164
Say why the CI install uses the checkout rather than a git+ URL
nirvedhmeshram Sep 11, 2026
dfe2c8f
Stop citing an Iris.allocate_symmetric method that does not exist
nirvedhmeshram Sep 11, 2026
a69502d
Note that ROCm 7.14+ ships rocSHMEM, so this build is temporary
nirvedhmeshram Sep 11, 2026
dd73aa8
Make the rocSHMEM init failure in CI diagnosable
nirvedhmeshram Sep 14, 2026
c1c820f
Add the ROCm 7.14 TODO and stop repeating the same comment three times
nirvedhmeshram Sep 15, 2026
6e8a77f
Suspend pytest capture so the rocSHMEM init abort says something
nirvedhmeshram Sep 15, 2026
f0ced7e
Build rocSHMEM for gfx950 too; the runner label understates the hardware
nirvedhmeshram Sep 15, 2026
3851e4a
Remove the rocSHMEM CI diagnostics
nirvedhmeshram Sep 15, 2026
233c840
Point back at Iris.allocate_symmetric now that it exists
nirvedhmeshram Sep 15, 2026
ed0c69e
Fail the Apptainer build on error instead of caching a broken image
nirvedhmeshram Sep 16, 2026
51d0b33
Serialize Apptainer image builds and publish atomically
nirvedhmeshram Sep 16, 2026
6d6da4d
Remove the manual multi-node script
nirvedhmeshram Sep 16, 2026
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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# The Docker build context is the repo root (see .github/scripts/container_build.sh)
# so that the Dockerfile can COPY in .github/scripts/install_rocshmem.sh. Nothing
# else in the tree is needed at image build time, and .git alone is tens of MB.
.git
.github/workflows
**/__pycache__
**/*.pyc
.pytest_cache
*.egg-info
build
dist
docs
examples
tests
.claude
82 changes: 58 additions & 24 deletions .github/scripts/container_build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,40 +43,71 @@ if [ "$CONTAINER_RUNTIME" = "apptainer" ]; then
DEF_CHECKSUM=$(sha256sum "$DEF_FILE" | awk '{print $1}')

# Create persistent Apptainer directory with checksum subdirectory
mkdir -p "${HOME}/iris-apptainer-images/${DEF_CHECKSUM}"
CACHE_DIR="${HOME}/iris-apptainer-images/${DEF_CHECKSUM}"
mkdir -p "$CACHE_DIR"

# Define paths
IMAGE_PATH="${HOME}/iris-apptainer-images/${DEF_CHECKSUM}/iris-dev.sif"
CHECKSUM_FILE="${HOME}/iris-apptainer-images/${DEF_CHECKSUM}/iris-dev.sif.checksum"
# Define paths. $HOME is shared across the runners, so every job in a run
# reads and writes this one directory.
IMAGE_PATH="$CACHE_DIR/iris-dev.sif"
CHECKSUM_FILE="$CACHE_DIR/iris-dev.sif.checksum"
LOCK_FILE="$CACHE_DIR/.build.lock"

# Check if image exists and has a valid checksum
REBUILD_NEEDED=true
if [ -f "$IMAGE_PATH" ] && [ -f "$CHECKSUM_FILE" ]; then
OLD_CHECKSUM=$(head -n1 "$CHECKSUM_FILE" 2>/dev/null)
image_is_current() {
[ -f "$IMAGE_PATH" ] && [ -f "$CHECKSUM_FILE" ] || return 1
local old
old=$(head -n1 "$CHECKSUM_FILE" 2>/dev/null)
# Validate checksum format (64 hex characters for SHA256)
if [[ "$OLD_CHECKSUM" =~ ^[a-f0-9]{64}$ ]] && [ "$OLD_CHECKSUM" = "$DEF_CHECKSUM" ]; then
echo "[INFO] Def file unchanged (checksum: $DEF_CHECKSUM)"
echo "[INFO] Skipping rebuild, using existing image at $IMAGE_PATH"
REBUILD_NEEDED=false
else
echo "[INFO] Def file changed (old: ${OLD_CHECKSUM:-<invalid>}, new: $DEF_CHECKSUM)"
echo "[INFO] Rebuilding Apptainer image..."
fi
else
echo "[INFO] Image or checksum not found, building new Apptainer image..."
fi
[[ "$old" =~ ^[a-f0-9]{64}$ ]] && [ "$old" = "$DEF_CHECKSUM" ]
}

# Build the image if needed
if [ "$REBUILD_NEEDED" = true ]; then
if apptainer build --force "$IMAGE_PATH" "$DEF_FILE"; then
# Build to a private path and rename into place. Renaming is atomic and
# leaves the inode alone, so a job already executing the old image keeps
# running against it; `apptainer build --force` straight to IMAGE_PATH would
# truncate the file out from under it.
build_image() {
local tmp
tmp=$(mktemp -u "$CACHE_DIR/.iris-dev.XXXXXXXX.sif")
if apptainer build --force "$tmp" "$DEF_FILE"; then
mv -f "$tmp" "$IMAGE_PATH"
# Store the checksum only if build succeeded
echo "$DEF_CHECKSUM" > "$CHECKSUM_FILE"
echo "[INFO] Built image: $IMAGE_PATH"
echo "[INFO] Checksum saved: $DEF_CHECKSUM"
else
rm -f "$tmp"
echo "[ERROR] Apptainer build failed"
exit 1
fi
}

if image_is_current; then
echo "[INFO] Def file unchanged (checksum: $DEF_CHECKSUM)"
echo "[INFO] Skipping rebuild, using existing image at $IMAGE_PATH"
else
echo "[INFO] No current image for this def file"
# Serialize builders. Without this, jobs that start together all see no
# image and all build concurrently into the same path -- observed, with
# two runners building at once. The re-check inside the lock is the
# point: whoever waits usually finds the image already built and skips a
# redundant half-hour build.
if command -v flock > /dev/null 2>&1; then
exec 9> "$LOCK_FILE"
if ! flock -w 5400 9; then
echo "[ERROR] Timed out waiting for the image build lock"
exit 1
fi
if image_is_current; then
echo "[INFO] Another job built it while we waited; using $IMAGE_PATH"
else
echo "[INFO] Building new Apptainer image..."
build_image
fi
exec 9>&-
else
echo "[WARN] flock not available; building without a lock"
echo "[INFO] Building new Apptainer image..."
build_image
fi
fi

elif [ "$CONTAINER_RUNTIME" = "docker" ]; then
Expand All @@ -89,8 +120,11 @@ elif [ "$CONTAINER_RUNTIME" = "docker" ]; then
echo "[INFO] Using existing Docker image: $IMAGE_NAME"
else
echo "[INFO] Docker image $IMAGE_NAME not found, building..."
DOCKER_DIR="$(dirname "$(realpath "$0")")/../../docker"
if docker build -t "$IMAGE_NAME" "$DOCKER_DIR"; then
REPO_ROOT="$(dirname "$(realpath "$0")")/../.."
# Build from the repo root, not docker/, so the Dockerfile can COPY in
# .github/scripts/install_rocshmem.sh -- the same installer the Apptainer
# def file pulls in via %files. A docker/-only context cannot see it.
if docker build -t "$IMAGE_NAME" -f "$REPO_ROOT/docker/Dockerfile" "$REPO_ROOT"; then
echo "[INFO] Built Docker image: $IMAGE_NAME"
else
echo "[ERROR] Docker build failed"
Expand Down
105 changes: 105 additions & 0 deletions .github/scripts/install_rocshmem.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/bin/bash
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
#
# Install rocSHMEM and its Python bindings, for tests/unittests/test_rocshmem_provider.py.
#
# Without this the provider tests skip rather than run: rocshmem4py has no
# prebuilt wheel anywhere (not PyPI, not the ROCm wheel indexes), and its
# bindings do not build rocSHMEM themselves -- python/rocshmem does
# find_package(rocshmem 3.5.0 CONFIG REQUIRED) with no FetchContent. So rocSHMEM
# has to be built first, and then the bindings against it.
#
# Scope is deliberately IPC-only: that is what the provider uses, and it is all
# a single-node CI runner can exercise. Upstream already defaults USE_IPC=ON and
# USE_GDA=OFF, so no conduit flags are passed -- which also keeps MPI and the
# RDMA provider libraries out of the picture entirely.
set -euo pipefail

ROCSHMEM_PREFIX="${ROCSHMEM_PREFIX:-/opt/rocshmem}"
# Both arches on purpose. The runner label says mi325 (gfx942) but the hardware
# reports gfx950, and rocSHMEM's device code must match the GPU it runs on: build
# for the wrong one and hipModuleGetGlobal fails on rocSHMEM's device globals
# ("Cannot create GlobalVar Obj for symbol: _ZN8rocshmem14logd_constantsE") and
# init aborts. Semicolon-separated cmake list.
ROCSHMEM_GPU_TARGETS="${ROCSHMEM_GPU_TARGETS:-gfx942;gfx950}"
ROCSHMEM_REPO="${ROCSHMEM_REPO:-https://github.com/ROCm/rocm-systems.git}"
ROCSHMEM_REF="${ROCSHMEM_REF:-develop}"
ROCM_PATH="${ROCM_PATH:-/opt/rocm}"
SRC="$(mktemp -d)"

# TODO: drop this source build once the CI base images reach ROCm 7.14+, whose
# artifacts ship rocSHMEM's static library and headers -- only the bindings below
# would still be needed. The bases are ROCm 7.2.1 (apptainer) and 7.1 (docker)
# today. rocshmem4py stays a source build either way until its TheRock packaging
# lands.
echo "==> rocSHMEM ${ROCSHMEM_REF} -> ${ROCSHMEM_PREFIX} (GPU_TARGETS=${ROCSHMEM_GPU_TARGETS})"

# rocm-systems is a large monorepo and we need two directories out of it. Sparse
# checkout keeps this from dominating image build time and size.
git clone --depth 1 --branch "${ROCSHMEM_REF}" --filter=blob:none --sparse \
"${ROCSHMEM_REPO}" "${SRC}"
git -C "${SRC}" sparse-checkout set projects/rocshmem python/rocshmem

[ -f "${SRC}/projects/rocshmem/CMakeLists.txt" ] || {
echo "ERROR: projects/rocshmem missing after sparse checkout" >&2; exit 1; }

# rocSHMEM's cmake/setup_project.cmake does a REQUIRED find_file for
# .info/version under ROCM_PATH. Images that lack that file fail to configure
# with "Could not find rocm_version_file", so use the documented escape hatch and
# read the version from rocm_version.h, which is authoritative. hipconfig
# --version is not used: it reports a build number that parses as the patch level.
EXPLICIT_ROCM_VERSION="${EXPLICIT_ROCM_VERSION:-}"
if [ -z "${EXPLICIT_ROCM_VERSION}" ] && [ ! -f "${ROCM_PATH}/.info/version" ]; then
_vh="$(find "${ROCM_PATH}" -name rocm_version.h 2>/dev/null | head -1)"
if [ -n "${_vh}" ]; then
EXPLICIT_ROCM_VERSION="$(awk '
/ROCM_VERSION_MAJOR/ {maj=$3} /ROCM_VERSION_MINOR/ {min=$3}
/ROCM_VERSION_PATCH/ {pat=$3}
END {if (maj != "") printf "%s.%s.%s", maj, min, pat}' "${_vh}")"
echo "==> ROCm ${EXPLICIT_ROCM_VERSION} detected from ${_vh}"
fi
fi

cmake -S "${SRC}/projects/rocshmem" -B "${SRC}/build" -G Ninja \
${EXPLICIT_ROCM_VERSION:+-DEXPLICIT_ROCM_VERSION="${EXPLICIT_ROCM_VERSION}"} \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${ROCSHMEM_PREFIX}" \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DGPU_TARGETS="${ROCSHMEM_GPU_TARGETS}"
cmake --build "${SRC}/build" --parallel "$(nproc)"
cmake --install "${SRC}/build"

# USE_IPC must be ON or rocshmem_ptr returns NULL for every peer and the provider
# refuses to build a table. It is the upstream default, so this asserts rather
# than sets it -- a silent flip would otherwise surface much later as skipped tests.
if ! grep -qi "define ROCSHMEM_USE_IPC\|USE_IPC" \
"${ROCSHMEM_PREFIX}"/include/rocshmem/*.hpp 2>/dev/null; then
echo "==> note: could not confirm USE_IPC from headers; provider will report at run time"
fi

# A pip install from source, same as the one-liner in iris/experimental/README.md
# but pointed at the checkout above instead of a git+ URL. That is deliberate: a
# git+ URL makes pip clone the monorepo again, independently, at whatever HEAD
# develop happens to be at -- so the bindings could be built from a different
# revision than the core installed above. find_package would not catch it, since
# it only compares versions, and the bindings statically link the core. One
# checkout for both makes the skew impossible, and saves a second clone.
#
# CMAKE_PREFIX_PATH is the documented way to point the bindings at an install;
# setup.py forwards it to CMake as a cache variable so a rocSHMEM shipped under
# /opt/rocm cannot shadow it. ROCSHMEM_HOME is no longer required.
echo "==> building rocshmem4py against ${ROCSHMEM_PREFIX}"
CMAKE_PREFIX_PATH="${ROCSHMEM_PREFIX}" ROCM_PATH="${ROCM_PATH}" \
pip3 install --no-cache-dir "${SRC}/python/rocshmem"

python3 -c "
import rocshmem4py, importlib.metadata as md
print(' rocshmem4py', md.version('rocshmem4py'), '->', rocshmem4py.__file__)
for n in ('rocshmem_my_pe', 'rocshmem_n_pes', 'rocshmem_ptr'):
assert hasattr(rocshmem4py, n), f'missing {n}'
print(' provider API present')
"

rm -rf "${SRC}"
echo "==> rocSHMEM install complete"
34 changes: 32 additions & 2 deletions apptainer/iris.def
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,25 @@
Bootstrap: docker
From: rocm/pytorch:rocm7.2.1_ubuntu24.04_py3.14_pytorch_2.10.0

# The rocSHMEM installer is copied in rather than inlined, so it stays one
# implementation shared with docker/Dockerfile. Caveat: container_build.sh caches
# the image on the checksum of THIS def file only, so editing
# install_rocshmem.sh alone reuses a stale image -- touch this file too.
%files
.github/scripts/install_rocshmem.sh /opt/install_rocshmem.sh

%post
/bin/bash -c "
# Fail the build on the first error. Without this a step can fail silently
# and still produce an image that Apptainer reports as built -- which
# container_build.sh then caches by def-file checksum and every later job
# reuses. That happened: a transient 'fetch-pack: unexpected disconnect'
# during the Triton clone left /opt/triton absent, so the pinned checkout and
# its editable install never ran, 'import triton' silently fell back to the
# older pytorch-triton-rocm in site-packages, and every Triton test failed
# far downstream with an unrelated-looking error.
set -e

# Set environment variables
export TRITON_PATH=/opt/triton
export ROCM_PATH=/opt/rocm
Expand All @@ -28,15 +45,27 @@ From: rocm/pytorch:rocm7.2.1_ubuntu24.04_py3.14_pytorch_2.10.0
pip3 install --upgrade pip && \
pip3 install wheel jupyter

# Clone and install Triton
# Clone and install Triton. Retried: this is a large clone and a transient
# disconnect here used to poison the cached image rather than fail the build.
cd /opt
git clone https://github.com/triton-lang/triton.git \$TRITON_PATH
for attempt in 1 2 3; do
rm -rf \$TRITON_PATH
git clone https://github.com/triton-lang/triton.git \$TRITON_PATH && break
echo \"triton clone failed (attempt \$attempt)\" >&2
[ \$attempt -lt 3 ] || exit 1
sleep 10
done
cd \$TRITON_PATH
git checkout f7c1d69401e9f09050451f30776562954b05e850
pip3 install -e .

# Make the venv writable by all
chmod -R 777 /opt/venv

# rocSHMEM + rocshmem4py, so the provider tests run instead of skipping.
# See install_rocshmem.sh for the rationale, including the two arches.
ROCSHMEM_GPU_TARGETS='gfx942;gfx950' ROCSHMEM_PREFIX=/opt/rocshmem \
bash /opt/install_rocshmem.sh
"

%environment
Expand All @@ -52,6 +81,7 @@ From: rocm/pytorch:rocm7.2.1_ubuntu24.04_py3.14_pytorch_2.10.0
export OMPI_ALLOW_RUN_AS_ROOT=1
# Set required RCCL environment variable for ROCm
export HSA_NO_SCRATCH_RECLAIM=1
export ROCSHMEM_PREFIX=/opt/rocshmem

%runscript
echo "Welcome to the ROCm-aware Apptainer image!"
Expand Down
7 changes: 7 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ RUN git checkout bcbcabdd0cff6539c7168299075992b2a23ff38e
RUN pip3 install -e .
ENV PYTHONPATH=$TRITON_PATH

# rocSHMEM + rocshmem4py, so tests/unittests/test_rocshmem_provider.py runs
# instead of skipping. See install_rocshmem.sh for why it is a source build and
# why it builds for two arches.
ENV ROCSHMEM_PREFIX=/opt/rocshmem
COPY .github/scripts/install_rocshmem.sh /tmp/install_rocshmem.sh
RUN ROCSHMEM_GPU_TARGETS='gfx942;gfx950' bash /tmp/install_rocshmem.sh && rm /tmp/install_rocshmem.sh

# Set up workspace
WORKDIR /workspace

Expand Down
Loading
Loading