Skip to content

Conda-friendly builds, AMD GPU support, and macOS OpenMP - #23

Merged
Sophia Wen (hfwen0502) merged 8 commits into
mainfrom
conda-build-amd-support
Sep 11, 2026
Merged

Conda-friendly builds, AMD GPU support, and macOS OpenMP#23
Sophia Wen (hfwen0502) merged 8 commits into
mainfrom
conda-build-amd-support

Conversation

@hfwen0502

Copy link
Copy Markdown
Collaborator

Supersedes fix-conda-build-and-mpi-detection and amd-rocm-omp-offload,
rebased onto main so #15, #16, #17 and #21 are preserved.

Build

  • Derive MPI_HOME from the MPI mpi4py is linked against, so the extension and
    mpi4py cannot disagree at runtime.
  • Make conda builds work: strip the gcc-only sysconfig tokens nvc++ rejects, and
    drop conda's -B compiler_compat while keeping its -L/-rpath.
  • SBD_GPU_ARCH is optional and honored at link as well as compile, so a
    multi-arch Thrust fatbin keeps every architecture (supersedes Pass -gpu= at link time so the Thrust fatbin keeps every architecture #17).

AMD GPUs

  • The OpenMP target-offload backend builds under amdclang++ as well as nvc++;
    SBD_GPU_VENDOR forces the choice on a host with both. Thrust stays
    NVIDIA-only, and SBD_BUILD_BACKEND=gpu fails on AMD rather than silently
    falling back to CPU.
  • On AMD the arch is detected with the amdgpu-arch beside the chosen compiler; a
    GPU-less build host is a hard error rather than an unrunnable module.
  • gpu-omp is vendor-neutral; get_backend('gpu-omp').__sbd_offload_target__
    reports what an install was built for.
  • device_config.auto() resolves against backends actually compiled, instead of
    returning the CUDA Thrust backend whenever any GPU was detected.

Backends load lazily, one per process, and all are built by default — no
second install, no silent GPU demotion.

macOS takes libomp and OpenBLAS from the conda env (Homebrew as fallback), and
the build prints which compiler and which libomp it chose.

Verified

  • MI250X / ROCm: 8 ranks, 4x2 grid, --device gpu-omp, correct energy.
  • macOS arm64 under Apple clang 21.0.0 and conda clang 23.1.0: no Homebrew
    references linked, pytest and the 2-rank MPI test green.

Not exercised: the gdb_diag offload-device pin — GDB has no example or test
anywhere yet.

Squashes the 22 commits of fix-conda-build-and-mpi-detection, rebased onto
main so that #15, #16, #17 and #21 are preserved rather than reverted.

setup.py
- Derive MPI_HOME from the MPI mpi4py is actually linked against, instead of
  guessing, so the extension and mpi4py cannot disagree at runtime.
- Make conda-based builds work: strip the gcc-only tokens RHEL 9 CPython bakes
  into sysconfig that nvc++ rejects, and drop conda's -B compiler_compat while
  keeping its -L/-rpath entries.
- Make SBD_GPU_ARCH optional and honor it at link as well as compile, so a
  multi-arch Thrust fatbin keeps every architecture it was asked for. This
  supersedes the narrower fix in #17.

python/__init__.py, device_config.py
- Load the backends lazily, one per process, and build all three by default.
  Removes the silent GPU demotion and the need for a second install.
- Report why a backend is missing, and fail instead of silently using the CPU
  when gpu-omp has no device.

README: rewritten around the conda recipe and split GPU prerequisites by build
vs run. Retains #15's point that the sdist bundles the headers.
Squashes the four commits of amd-rocm-omp-offload.

The same _core_gpu_omp_offload source now builds under either vendor: nvc++
-mp=gpu for NVIDIA, amdclang++ --offload-arch=gfx* for AMD. Thrust stays
NVIDIA-only, since upstream wires it to nvc++ -cuda and there is no rocThrust
configuration to build; SBD_BUILD_BACKEND=gpu therefore fails on AMD rather
than silently falling back to the CPU.

setup.py
- detect_gpu_toolchain() picks nvidia or amd, with SBD_GPU_VENDOR to force the
  choice on a host carrying both; find_rocm_toolchain() honors ROCM_HOME then
  PATH, mirroring find_nvidia_hpc_sdk().
- SBD_GPU_ARCH is spelled per vendor and applied at compile and link: nvc++
  takes -gpu=cc80,cc90 as one flag, clang needs a repeated --offload-arch=.
  Unset on AMD it is detected with the amdgpu-arch that ships beside the chosen
  compiler (not one found on PATH, which can come from a different ROCm), and a
  GPU-less build host is a hard error rather than an unrunnable module.
- amdclang++ needs far less sysconfig scrubbing than nvc++: only
  -fcf-protection, and -march=x86-64-v2 is deliberately left alone.

python/
- 'gpu-omp' is vendor-neutral; get_backend('gpu-omp').__sbd_offload_target__
  reports what an install was actually built for, and the Thrust backend is
  stamped the same way. rocm/gpu-amd-omp/gpu-rocm-omp are aliases.
- device_config.auto() resolves against the backends actually COMPILED instead
  of returning 'gpu' whenever any GPU was detected, which selected the CUDA
  Thrust backend on AMD hosts. GPU presence is probed with rocm-smi as well as
  nvidia-smi.
- bindings.cpp pins each rank to one offload device, reading the vendor's
  device-visibility variable when omp_get_num_devices() reports 0 under dlopen.

README: one rule for MPI -- every GPU backend hands MPI device pointers, so use
a GPU-aware MPI (CUDA-aware on NVIDIA, ROCm-aware on AMD).

Verified on MI250X (gfx90a, ROCm): 8 ranks, 4x2 grid, --device gpu-omp.
sbd_pin_offload_device() was wired into tpb_diag and tpb_diag_from_files but
not gdb_diag, so with --device gpu-omp every rank ran gdb_diag on the default
device -- the "all ranks land on GPU 0" symptom, for that entry point only.

Not caught earlier because the three call sites are copies rather than one
helper; folding them together is a separate cleanup.
- Per-probe timeouts in _gpu_available(): rocm-smi keeps the 30 s it needs to
  enumerate a multi-GCD node, but nvidia-smi no longer inherits it. A wedged
  NVIDIA driver hangs rather than failing, and it was stalling backend
  selection for 30 s instead of 5.

- Require a GPU[<n>] line from `rocm-smi --showid` rather than trusting its
  exit status, in both _gpu_available() and DeviceConfig._check_hip(). A ROCm
  install on a GPU-less BUILD host is exactly where a bare `rocm-smi` exiting 0
  claims hardware that is not there, after which auto() picks a backend that
  cannot run. This is the same signal get_device_info() already counts devices
  with, so there is now one convention instead of two.

- find_rocm_toolchain() honors ROCM_PATH as well as ROCM_HOME, checked in that
  order so an explicit ROCM_HOME still wins. ROCM_PATH is the variable ROCm
  itself sets, and tox.ini already passed it through to a build that ignored it.
The Darwin block hardcoded Homebrew paths, so a conda env carrying llvm-openmp
still compiled against /opt/homebrew -- while at import time the process loads
the conda copies anyway, resolved through the interpreter's rpath. Prefer
$CONDA_PREFIX when it has include/omp.h; Homebrew stays as the fallback.

Two diagnostics, since this was hard to see:
- fail with the fix when neither source has libomp, rather than "'omp.h' file
  not found" from the middle of a compile
- print the resolved compiler and its version. sysconfig records a bare
  clang++, which PATH resolves, so a Homebrew LLVM silently wins over both
  Apple clang and a conda toolchain.

README: llvm-openmp in both conda recipes, a note to pin CC/CXX, and a
troubleshooting entry for a conda compiler package shadowing nvc++/amdclang++.

Verified on macOS arm64 under Apple clang 21.0.0 and conda clang 23.1.0: no
Homebrew references in the linked extension, pytest and the 2-rank MPI test
green in both environments.
Deriving the MPI prefix from mpi4py dropped main's mpicc probe, which broke CI:
Debian splits Open MPI's headers to /usr/lib/<triple>/openmpi/include, outside
the prefix, so mpi4py names the right MPI but $prefix/include/mpi.h does not
exist and the build gave up.

A prefix without mpi.h is an unusual layout, not the wrong MPI, so ask mpicc
before failing -- both when the prefix lacks the header and when no prefix is
found at all. mpi4py stays the preferred source, so the extension and mpi4py
still cannot diverge.
--showme:compile is an Open MPI-ism. MPICH's wrapper treats it as a source file
and tries to compile it (exit 127 here), so probing that first silently ruled out
MPICH -- which this package claims to support.

Probe `-show` first, which both wrappers understand and which prints the whole
command line, and keep --showme as the second attempt. Duplicate -I/-L entries
are collapsed (MPICH repeats them), and the libraries the wrapper names are used
as-is: -lmpi for Open MPI, -lmpi -lpmpi for MPICH.

Verified against conda-forge openmpi 5.0.10 and mpich 5.0.1.
-show is understood by every wrapper checked -- homebrew open-mpi, conda-forge
openmpi 5.0.10, conda-forge mpich 5.0.1 -- while --showme:compile works on the
Open MPI ones and exits 127 on MPICH. So -show strictly dominates, and the second
attempt was dead weight: one subprocess call instead of up to three.
@hfwen0502
Sophia Wen (hfwen0502) merged commit cf53fb7 into main Sep 11, 2026
6 checks passed
@hfwen0502
Sophia Wen (hfwen0502) deleted the conda-build-amd-support branch September 11, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant