Containers
Experimental
Container support is experimental. Single-node runs are tested (CPU, NVIDIA/AMD GPU, and multi-rank MPI); multi-node MPI is cluster-specific — see Running on HPC Clusters.
Pre-built Apptainer/Singularity containers let you run THOR without compiling from source.
Quick Start
thor-generic is the recommended image — same SIF runs on CPU, NVIDIA, or AMD GPU.
Registry access
thor-generic is currently a private package — log in first with
apptainer registry login --username <github-user> docker://ghcr.io
(use a GitHub PAT with read:packages). The thor-omp (CPU only) image is public
and needs no login.
Pick the backend at exec time:
# Download
apptainer pull oras://ghcr.io/cbyrohl/thor-generic:dev
# CPU (OpenMP via SSCP JIT)
apptainer run --env ACPP_VISIBILITY_MASK=omp thor-generic_dev.sif config.yaml
# NVIDIA GPU
apptainer run --nv --env ACPP_VISIBILITY_MASK=cuda thor-generic_dev.sif config.yaml
# AMD GPU
apptainer run --rocm --env ACPP_VISIBILITY_MASK=hip thor-generic_dev.sif config.yaml
Set device: in your config YAML to match — cpu, cpu-openmp, or gpu.
Updating an existing image
The :dev tag is updated in place whenever a new build is published, so your local .sif can fall behind. apptainer pull won't overwrite the existing file by default — pass --force:
Setting Thread Count
Control the number of OpenMP threads with OMP_NUM_THREADS:
apptainer run --env OMP_NUM_THREADS=8 --env ACPP_VISIBILITY_MASK=omp \
thor-generic_dev.sif config.yaml
MPI (multiple ranks)
THOR parallelises MCRT across MPI ranks. The thor-generic image ships a full
Open MPI 4.1 stack (launchers included), so a single-node, multi-rank job
runs entirely inside one container — the container's own mpirun launches the
ranks:
apptainer exec --cleanenv --env OMP_NUM_THREADS=8 --env ACPP_VISIBILITY_MASK=omp \
thor-generic_dev.sif mpirun --map-by slot:PE=8 --bind-to core -np 3 /opt/thor/bin/thor config.yaml
- Rank↔core binding (
--map-by slot:PE=N --bind-to core) — this is the part that matters for hybrid MPI+OpenMP. By default Open MPI pins each rank to a single core, so theOMP_NUM_THREADSOpenMP threads stack onto that one core (measured ~2.6× slower).--map-by slot:PE=Ninstead gives each rank its own N physical cores (setN = OMP_NUM_THREADS), and AdaptiveCpp honours the resulting cpuset — so the threads spread with proper locality. Constraint:N × ranks ≤ physical cores. Simpler fallback:--bind-to noneleaves ranks unpinned so the threads spread across the whole node — correct thread count and it fixes the slowdown, but with no per-rank NUMA locality (fine on a single socket, suboptimal across sockets). Use it when the core count is unknown or PE-mapping is awkward. --cleanenvstops the host's OpenMPI MCA environment from leaking into the container, which can crashMPI_Initon GPU-less nodes.--env OMP_NUM_THREADS: bound the OpenMP threads per rank.--cleanenvwipes any hostOMP_NUM_THREADS, so without this each of the-np 3ranks defaults to all cores and oversubscribes the node. Set it to roughly cores-per-node ÷ ranks.- Cold JIT cache: with the SSCP/JIT
thor-genericimage, ranks racing on AdaptiveCpp's shared kernel cache can spamCould not remove kernel cache file. Give each rank its own cache withACPP_APPDB_DIR=…_rank_${OMPI_COMM_WORLD_RANK}(e.g. wrap the launch inbash -c) — seeregression_tests/CI/shellmodel/test_mpi_container.shfor the pattern. (The AOTthor-ompimage has no JIT cache and is unaffected.) - Set
device: cpu-openmpin your config to matchACPP_VISIBILITY_MASK=omp. - Multi-rank support is per dataset: uniform grid and point-cloud (gas) domain-decompose across ranks; analytical models (e.g. the shell model, as the container MPI regression exercises) replicate the domain and split photons across ranks; octree and PSS abort with more than one rank.
For multi-node runs the launcher must sit outside the container so it can
span nodes (srun --mpi=pmix apptainer exec … thor). That path needs the host
MPI to be PMIx-compatible with the container's Open MPI 4.1 — see
Running on HPC Clusters.
Regression test
regression_tests/CI/shellmodel/test_mpi_container.sh runs the shellmodel
MCRT regression as a 3-rank job in a container. Point it at an image with
THOR_SIF=/path/to/thor-generic.sif bash test_mpi_container.sh.
GPU usage
thor-generic carries both the CUDA and ROCm runtimes in one image; backend selection happens at exec time, not build time.
NVIDIA
Requirements:
- NVIDIA driver installed on the host
- The
--nvflag (binds host GPU drivers into the container) device: gpuin your config YAML
AMD
Requirements:
- ROCm-supported AMD GPU (Instinct MI-series, supported Radeon Pro / RX)
amdgpukernel module loaded (/dev/kfdand/dev/dri/renderD*must exist)- The
--rocmflag device: gpuin your config YAML
Fedora hosts: libnuma ABI conflict with --rocm
Apptainer's --rocm binds the host's libnuma.so.1. On Fedora ≥ 39 (glibc ≥ 2.40) this conflicts with the Ubuntu 24.04 glibc inside the container (GLIBC_ABI_GNU2_TLS not found) and every acpp backend fails to load. Workaround:
Unsupported AMD targets
ROCm ships device-libs bitcode only for officially supported gfxes. Consumer iGPUs (e.g. gfx1036 on Ryzen 9000 desktop) hit Code object construction failed at JIT time. Sometimes --env HSA_OVERRIDE_GFX_VERSION=10.3.0 (gfx1030) works as a hack; for production use, target a supported device.
Selecting FP32 or FP64
The GPU-capable images ship two binaries side-by-side — FP64 (thor-fp64, default) and FP32 (thor-fp32) — and a wrapper that picks one based on the THOR_PRECISION env var:
# FP64 (default)
apptainer run --nv --env ACPP_VISIBILITY_MASK=cuda thor-generic_dev.sif config.yaml
# FP32 — typically much faster on consumer GPUs (RTX, GTX, RDNA),
# which have crippled FP64 throughput
apptainer run --nv --env ACPP_VISIBILITY_MASK=cuda --env THOR_PRECISION=fp32 \
thor-generic_dev.sif config.yaml
FP32 is reduced-precision
Float32 is sufficient for many MCRT runs but can degrade results in long photon chains, high optical depths, or tight numerical tolerances. Validate against an FP64 reference for your problem before relying on FP32 production runs.
Available Images
| Image | Description | Precision | Access |
|---|---|---|---|
thor-generic |
Portable SYCL build. CUDA + ROCm runtimes bundled; backend chosen at exec time. | FP64 + FP32 | private |
thor-omp |
CPU-only AOT build (AdaptiveCpp OMP backend). Smaller; best raw CPU perf. | FP64 | public |
thor-env-only |
Build environment (no thor binary) — for compiling THOR yourself. |
— | private |
Building Custom Containers
For Development Only
Most users should use the pre-built thor-generic image above.
pip install typer pyyaml jinja2
cd apptainer
# List configurations
./build.py list
# Build container
./build.py build omp
# Build from local source (requires initialized submodules)
./build.py build omp --local
Cluster Configurations
Configurations in apptainer/clusters/:
| Configuration | Description |
|---|---|
generic.yaml |
Portable SSCP build with CUDA + ROCm runtimes; FP64 + FP32 binaries |
omp.yaml |
OMP AOT backend, FP64, smaller image |
env-only.yaml |
Build environment only (no THOR binary) |
example-hpc.yaml |
Template for custom cluster-specific configs |
Create a custom config:
cp apptainer/clusters/example-hpc.yaml apptainer/clusters/my-cluster.yaml
# Edit march for your CPU (e.g., znver3 for AMD EPYC Milan)
./build.py build my-cluster
Common march values: x86-64-v3 (generic), znver2/znver3/znver4 (AMD EPYC), skylake-avx512/icelake-server (Intel Xeon).
Shipping FP64 + FP32 binaries
Set dual_precision: true in your cluster YAML to build both binaries into the
same image with a THOR_PRECISION runtime dispatch wrapper (see
Selecting FP32 or FP64 above). The second cmake
build runs in build-fp32/ with -DTHOR_USE_FP32=ON; on-disk overhead is
small (~10 MB compressed) because the two binaries share most of their code.
Useful for any GPU-targeted config; off by default for CPU configs since FP32
yields little benefit on the CPU path.
Environment Container
For development without rebuilding containers:
./build.py build env-only
# Build and run THOR from source
apptainer exec build/thor-env-only.sif bash -c \
"cd /path/to/thor && cmake -B build && cmake --build build -j\$(nproc)"
apptainer exec build/thor-env-only.sif /path/to/thor/build/src/thor config.yaml
Accessing Host Data
By default, Apptainer only mounts your home directory and a few system paths inside the container. If your simulation data or Cloudy tables live elsewhere (e.g. /virgotng/, /scratch/, /data/), you must bind-mount those paths:
# Bind a single path
apptainer run --bind /virgotng thor-generic_dev.sif config.yaml
# Bind multiple paths
apptainer run --bind /virgotng,/scratch thor-generic_dev.sif config.yaml
Alternatively, set the APPTAINER_BIND environment variable (useful in job scripts):
For full details, see the Apptainer Bind Paths documentation.
Troubleshooting
fuse2fs not found / gocryptfs not found warnings: These informational messages from Apptainer can be safely ignored. THOR containers use Docker-based images and do not require EXT3 filesystem mounting or encrypted overlays.
Slow performance: Check march matches your CPU. Set OMP_NUM_THREADS appropriately.