Apptainer images for Slurm¶
Slurm execution uses Apptainer SIF files on shared storage. Three image roles cover the HEC-RAS workflow; each is built or published as an OCI image and converted to a SIF once, then pinned by its SHA-256.
| Role | Image | Used by | Recipe |
|---|---|---|---|
| Native Linux unsteady solver | rascommander/hec-ras-linux-unsteady_<version>:v1 |
RasApptainer.render_job |
containers/hecras-unsteady |
| Wine preprocessing (Docker workflow) | rascommander/hec-ras-wine-precompute_<version>:v4 |
RasDocker.preprocess_plan |
containers/hecras-wine-precompute |
| Wine portable executor | Derived from the pre-compute image | RasSlurm, RasApptainer.render_prepare_job, RasPortableDocker |
containers/hecras-wine-executor |
Qualified images for every supported release are published at ghcr.io/gpt-cmdr: OCI images
as hec-ras-<role>_<version> and byte-identical SIFs (ORAS) as hec-ras-<role>-sif_<version>,
tag 0.106.0. The packaged catalog pins each by digest and records the SIF's SHA-256, so
resolve_sif() and ensure_sif_command() pull the published SIF and verify it.
The pre-compute image is a Docker controller with its own entry point; under Apptainer, use the executor derived from it. The container catalog lists published registry digests. A registry digest and a SIF SHA-256 are different identities: jobs verify the SIF bytes.
Which images apply to which HEC-RAS release¶
| HEC-RAS | Full Windows compute under Wine (wine-executor; RasSlurm) |
Wine preprocessing → native Linux solve (wine-precompute + linux-unsteady; RasDocker, RasApptainer) |
|---|---|---|
| 5.0–5.0.6, 6.0 | Applies (one setting) | Not applicable: HEC published no Linux engine |
| 5.0.7 | Applies (one setting) | Applies: Linux engine uses the bin_ras layout (rasUnsteady64 plus the .c## computed geometry; see HEC-RAS 5.0.7) |
| 6.1 | Applies | Applies: canonical RasUnsteady + libs/ layout with the pre-6.3.1 solver arguments (RasUnsteady <project>.c## b##; see solver argument conventions) |
| 6.2, 6.3, 6.3.1, 6.4.1 | Applies | Not applicable: HEC published no Linux engine |
| 6.5, 6.6, 7.x | Applies | Applies |
HEC-RAS 4.x is not supported in containers: it has no command-line compute and runs only through its COM interface on Windows (see the legacy COM interface).
RasContainerImages.applicable_roles(version) returns the same answer in code.
An applicable role still needs an image built for that release:
build_release_images.sh builds the Wine images for any
install-folder version (5.0, 5.0.7, 6.3.1, 6.6, 7.0.1, …) from HEC's official
installer, and the native recipe builds the Linux engine image from HEC's Linux
distribution. Published images are listed in the packaged catalog
(RasContainerImages.catalog()) and in the container catalog.
Use a local or cached image¶
Jobs always run a SIF on shared storage that is verified by its SHA-256.
RasContainerImages.resolve_sif picks it, in this order:
- Your own image —
sif_path=(a site-built or locally cached SIF), used as-is after hashing; passexpected_sha256=to pin it. - The cache —
cache_dir=, else$RAS_COMMANDER_IMAGE_CACHE, else~/.cache/ras-commander/images. Files are named by registry digest and re-verified on every use (against the published SIF hash, or the.sha256sidecar recorded when the file was pulled). - The registry — pulled into the cache only if
pullallows:"never"(offline; fail if not cached),"missing"(default), or"always".
from ras_commander import RasContainerImages
from ras_commander.RasSlurm import SlurmSiteConfig
native = RasContainerImages.resolve_sif(
"linux-unsteady", "6.6", cache_dir="/shared/ras-images", pull="never",
)
executor = RasContainerImages.resolve_sif(
"wine-executor", "5.0.6", sif_path="/shared/ras-images/hec-ras-wine-executor_5.0.6.sif",
)
site = SlurmSiteConfig(
site_name="example", apptainer_image=str(executor.path),
apptainer_image_sha256=executor.sha256, container_identity=executor.container_identity,
)
When you submit from a workstation over SSH, fill the cluster's shared cache
from its login host, or from a short job, instead. ensure_sif_command returns an idempotent bash
snippet that reuses a verified copy or pulls one, and prints <sha256> <path>:
from ras_commander import RasApptainer, RasContainerImages, SshApptainerTransport
command = RasContainerImages.ensure_sif_command("linux-unsteady", "6.6", "/shared/ras-images")
result = SshApptainerTransport(profile).run(["bash", "-c", command])
sha256, path = result.stdout.split()
Pass work_dir= (for example /scratch/$SLURM_JOB_ID/apptainer-work) to unpack
image layers on node-local disk when the cache is on network storage; it defaults
to <cache>/.apptainer-work. Point the cache at storage that compute nodes can
read so one copy serves every job. Cache entries are never overwritten in place; a new release has a new
digest and a new file.
Get a SIF manually¶
Pull by digest. On a node with registry access, pull once to shared storage and record the SIF hash:
apptainer pull /shared/images/hec-ras-linux-unsteady_6.6-v1.sif \
docker://rascommander/hec-ras-linux-unsteady_6.6@sha256:71dec03350996cfa8a0ccb18ae08075de9b9cc3d7fb6b1dddbf541c030abb95c
sha256sum /shared/images/hec-ras-linux-unsteady_6.6-v1.sif
Convert a docker save archive. For locally built images (such as the
executor) or clusters without registry access, save the image with a hash
sidecar and convert it with build_sif_from_archive.sbatch as an
ordinary unprivileged Slurm job:
sbatch --account=<account> --partition=<partition> \
--export=ALL,RAS_COMMANDER_SIF_NAME=hec-ras-wine-executor_6.6-r1,RAS_COMMANDER_SIF_SRC=/shared/images/hec-ras-wine-executor_6.6-r1.tar.gz,RAS_COMMANDER_SIF_DEST=/shared/images \
containers/apptainer/build_sif_from_archive.sbatch
The job checks the archive against its .sha256 sidecar, decompresses it
(Apptainer's docker-archive:// reader cannot read gzip), builds in job
scratch with docker-archive:// and falls back to oci-archive://, runs a
smoke command inside the new SIF, and publishes <name>.sif,
<name>.sif.sha256, and a <name>.sif.json record (ras-commander-sif-record/v1,
binding the SIF digest to the OCI image ID and source archive). It never
overwrites a file and removes partial output on failure. It needs no Docker
daemon, --fakeroot, or privileged container, which also suits unprivileged
LXC compute nodes.
Put the SIF hash in the site configuration (apptainer_image_sha256,
prepare_image_sha256) and sif:sha256:<hash> in container_identity.
Never pull or convert per job.
Build Wine images for a release¶
containers/hecras-wine-runtime/build_release_images.sh builds the Wine images for
one HEC-RAS release from its official installer on a Linux Docker host:
CI=1 python -m pip wheel --no-deps --wheel-dir /tmp/rc-wheel .
bash containers/hecras-wine-runtime/build_base.sh # once: Wine 11, .NET 4.8, Windows Python 3.11
RAS_COMMANDER_COMMIT=$(git rev-parse HEAD) \
bash containers/hecras-wine-runtime/build_release_images.sh 5.0.7 HEC-RAS_507_Setup.exe \
/tmp/rc-wheel/ras_commander-<version>-py3-none-any.whl work/ archives/
- Runtime profile (
build_runtime_profile.sh): installs the release into a copy of the shared base prefix, installs the wheel into Windows Python, and writesruntime.jsonwith SHA-256 records for Python, the wheel,Ras.exe, and the installer. 7.x combined installers install nothing silently; extract their MSI first withextract_installer.pyand pass it asHECRAS_MSI=<file.msi>. - Pre-compute image (
hec-ras-wine-precompute_<version>:v5-<commit>): the bundled runtime, the CPU shim, and theRasDockercontroller. - Executor image (
hec-ras-wine-executor_<version>:<commit>), derived from step 2 as described below. docker savearchives with.sha256and.image-idsidecars for conversion to SIF. The pre-compute archive is kept only for releases with a Linux engine. The release's example projects are exported beside them ashec-ras-examples_<version>_<commit>.tar.gz(test data, not part of any image).
Each release needs about 20 GB free on the Docker host while it builds; the script removes its
images and BuildKit cache when done unless KEEP_IMAGES=1.
HEC-RAS 6.0 and earlier¶
For 6.0 and earlier (including 5.0.7), set RAS_COMMANDER_WINE_ACCEPT_TCU=1 in each task;
under Apptainer pass it as APPTAINERENV_RAS_COMMANDER_WINE_ACCEPT_TCU=1 in the site or
profile environment. Without it the executor and prepare job stop with an explanation.
Wine executor image¶
build_executor_image.sh derives the executor from a pre-compute image:
a digest-pinned rascommander/hec-ras-wine-precompute_<version> (6.5, 6.6, or 7.0.1) or a
locally built one passed by image ID, as build_release_images.sh does. It installs one ras-commander wheel into both the Linux Python
(with the compute extra) and the Windows C:\Python311 in the Wine prefix,
adds the GDAL bridge C:\Python311\GDAL → HEC-RAS\<version>\GDAL that
RasMapper calls need (when the release ships that folder), installs the CPU shim, and sets
the executor environment:
| Variable | Value |
|---|---|
RAS_COMMANDER_WINE_PREFIX_SEED |
/runtime/wine-seed/prefix |
RAS_COMMANDER_WINE_PYTHON |
C:\Python311\python.exe |
RAS_COMMANDER_WINE_ARCH |
win64 |
RAS_COMMANDER_WINE_CPU_SHIM |
/opt/ras-commander-executor/cpushim.so |
CI=1 python -m pip wheel --no-deps --wheel-dir /tmp/rc-wheel .
RAS_COMMANDER_COMMIT=$(git rev-parse HEAD) \
bash containers/hecras-wine-executor/build_executor_image.sh 6.6 \
rascommander/hec-ras-wine-precompute_6.6@sha256:5fbc07a20693cb867578048be632d88cee02618780259c1c7ddc258c9fbb4318 \
/tmp/rc-wheel/ras_commander-<version>-py3-none-any.whl \
local/hec-ras-wine-executor_6.6:r1
docker save local/hec-ras-wine-executor_6.6:r1 | gzip -1 > /shared/images/hec-ras-wine-executor_6.6-r1.tar.gz
The build runs on a Linux Docker host (BuildKit cannot run Wine, so the script runs and commits one container). It labels the image with the wheel hash, source commit, and parent digest, then verifies both Pythons import the same ras-commander version from a fresh prefix copy with networking disabled. That verification is an import check, not execution evidence. The script runs on a build host, not a compute node; convert the archive as above.
Runtime contract under Apptainer¶
- The SIF stays read-only. No
--writable,--writable-tmpfs, or overlay. - Each task copies the baked prefix into its own directory under
/tmp, which must be a node-scratch bind because--containreplaces the host/tmpwith a small session directory.RasSlurmandrender_prepare_jobbind a per-step node-scratch directory as/tmpautomatically. - Windows steps run under
xvfb-run -n <display>with a display number probed free on the node, because contained steps cannot see each other's/tmplock files; no display is needed. - The task-private
wineserveris awaited (and killed if it does not exit) before the prefix is removed. --cleanenvdrops the host environment; the image'sRAS_COMMANDER_WINE_*variables still apply.
CPU-numbering shim¶
In a Slurm or cgroup CPU set that does not start at CPU 0 (for example CPUs
4–5), Wine reports two processors but returns the raw host CPU ids as the
Windows processor number. The .NET runtime used by RasProcess.exe indexes
per-processor state with that number, and HEC-RAS preprocessing fails
intermittently with 0xC0000005 in clr.dll. In CLB testing, 6 of 12 runs
crashed without a fix and 0 of 24 with it.
cpushim.c is an LD_PRELOAD library that maps sched_getcpu() to
the CPU's index within the process affinity mask. The executor and
apptainer_prepare preload it for the Wine launch (xvfb-run, Xvfb,
wineserver, and the Wine processes; CPUs outside the mask pass through
unchanged) when RAS_COMMANDER_WINE_CPU_SHIM names it; an empty or 0 value disables it, and
a value that is not a regular absolute file fails before any Wine state is
created. Both also disable the Wine debugger so a crash exits instead of
waiting for input.
The published pre-compute v4 images do not contain the shim; the executor
build compiles it when the base lacks it. The v5 pre-compute images built by
build_release_images.sh bake it in.
The executor and prepare job also set KMP_AFFINITY=disabled unless it is already set:
HEC-RAS 7.0's OpenMP runtime otherwise aborts under Wine with OMP: Error #179.
Qualification status¶
Live qualification on October 13–14, 2026 (ras-commander PRs #525/#528, Apptainer 1.5.4, Slurm
partition clb). For every release, the executor was built by build_release_images.sh from
HEC's official installer, converted with build_sif_from_archive.sbatch, and run as a RasSlurm
job array computing the release's own Muncie example, plans 01 (1D) and 03 (2D), under Wine with
validated receipts (solver verified, volume accounting and inflow reconciliation passed). Where a
Linux engine exists, render_prepare_job prepared plan 03 under Wine in the executor SIF and
render_job solved it with the native SIF on 2 cores; the table compares the native water surface
with the Wine result over computational cells.
| HEC-RAS | Wine compute, plans 01 + 03 | Native solve, plan 03 | Max WSE difference, any time step | Max-WSE map difference |
|---|---|---|---|---|
| 7.0.1 | Passed | Passed, 30 s | 0.010 ft | 0.0001 ft |
| 7.0 | Passed | Passed, 32 s | 0.008 ft | 0.0005 ft |
| 6.6 | Passed | Passed, 24 s | 0.010 ft | 0.0001 ft |
| 6.5 | Passed | Passed, 25 s | 0.011 ft | 0.0001 ft |
| 6.4.1, 6.3.1, 6.3, 6.2 | Passed | No Linux engine | — | — |
| 6.1 | Passed | Passed, 40 s | 0.009 ft | 0.0001 ft |
| 6.0 | Passed | No Linux engine | — | — |
| 5.0.7 | Passed | Passed, 74 s | 0.012 ft | 0.0001 ft |
| 5.0.6, 5.0.5, 5.0.4, 5.0.3, 5.0.1, 5.0 | Passed | No Linux engine | — | — |
Gridded precipitation¶
BaldEagleCrkMulti2D plan 06 ("Gridded Precip - Infiltration", HEC's example from 6.0 on) was
run with two precipitation sources: the bundled NEXRAD DSS grid (run window 2018-09-09
18:00-19:20), and three synthetic hourly GeoTIFF frames imported with
RasUnsteady.set_gridded_precipitation_geotiff (ras-commander writes the NetCDF and the
"Imported Raster Data" payload HEC-RAS's GUI import would write). The check is the cumulative
precipitation depth that reached the mesh.
| HEC-RAS | DSS grid, Wine | DSS grid, native | GeoTIFF, Wine | GeoTIFF, native |
|---|---|---|---|---|
| 7.0.1, 7.0, 6.6, 6.5 | Passed, 0.11454 in | Passed, identical | Passed, 0.11575 in | Passed, identical |
| 6.4.1, 6.3.1, 6.3, 6.2 | Passed, 0.11454 in | No Linux engine | Passed, 0.11575 in | No Linux engine |
| 6.1 | Passed, 0.11454 in | Passed, identical | Passed, 0.10978 in | Passed, identical |
| 6.0 | Passed, 0.11454 in | No Linux engine | Passed, 0.10978 in | No Linux engine |
The DSS results match the Windows reference for this window; the GeoTIFF results for 6.2 and
later match the Windows/Wine 6.6 reference. 6.0 and 6.1 map the same raster to the mesh
differently (identically under Wine and natively), so compare raster results within a release.
A plan whose .u## names a GDAL raster without the imported payload fails in every release
(Expected precip dataset ... Imported Raster Data/Values does not exist): HEC-RAS reads GDAL
rasters only in its GUI import, so import them with ras-commander. HEC-RAS 5.x has no gridded
meteorology.
RasDocker with the final images ran Muncie plan 03 end to end (Wine preparation, native
solve) for 7.0.1, 7.0, 6.6, 6.5, 6.1, and 5.0.7, and the DSS case for 7.0.1, 7.0, 6.6, 6.5,
and 6.1, with the same cumulative precipitation. The SIFs in the packaged catalog are the
exact files of the final Slurm requalification (2026-10-15): for every Linux-engine release,
Muncie plans 01/03 under Wine and natively, and the DSS case under Wine and natively. Hosts whose Docker AppArmor profile denies Wine's sockets need
apparmor_profile="unconfined".
Notes:
- The differences are between engines (Windows under Wine on 1 core, Linux on 2 cores), not
run-to-run noise. 7.x writes engine-specific placeholder values in trailing boundary cells, so
compare computational cells only (
Geometry/2D Flow Areas/AttributesCell Count). - HEC-RAS 5.x writes volume accounting as
Vol Accounting …attributes onResults/Unsteady/Summaryrather than aVolume Accountinggroup; receipts validate either. - The 6.6 executor was also run in pool mode with identical results, and 436 steady 6.6 requests
ran earlier through
RasSlurm(see Portable execution on Slurm). - Each Wine step's memory peak reached about 6 GiB (cgroup accounting, including the private
prefix copy and file cache); budget
slurm_memoryat 6–8 GiB per Wine request and measure on your models. TACC was not run.
That is one example project per release. Build and convert the images, then run one representative model end to end on your cluster before a campaign. A successful job is computational evidence, not engineering approval.