Runs the SCHISM ocean model's Test_CORIE example (Columbia River estuary,
28-day real-tide/atmospheric-forcing hindcast) on a single cloud VM via
SkyPilot, building SCHISM from source with UGRID-compliant
output, and uploads its own output to S3 as part of the run. A separate
post-processing script turns that S3 output into a queryable virtual
Icechunk store, explored in a notebook with xarray +
xugrid + hvplot.
pip install "skypilot[aws,gcp]"
sky check # confirm at least one cloud is enabled
aws s3 mb s3://<your-bucket-name> --region us-east-1
./scripts/setup-runner-role.sh <your-bucket-name> # persistent, scoped IAM role -- see belowIf you use a bucket name other than the default baked into schism-corie.yaml
(envs.BUCKET), update that value too.
sky launch --dryrun schism-corie.yaml # sanity-check + cost estimate
# Smoke test: shortened 1-day run to validate the whole pipeline first
sky launch -y -c schism-corie schism-corie.yaml --env RNDAY_OVERRIDE=1
# Full 28-day run (reuses the cached build/data via sky exec)
sky exec schism-corie schism-corie.yaml --env RNDAY_OVERRIDE=28
# Output is already in S3 by the time the run above finishes (see "What
# this does"). Tear down when done -- though the cluster also auto-tears
# itself down after 30 idle minutes regardless (see below).
sky down schism-corie
# Post-processing (run locally, not on the cluster): turn that run's S3
# output into a virtual Icechunk store -- no data copied, just byte-range
# references. Needs its own deps: pip install virtualizarr icechunk obstore
# obspec-utils xarray boto3
AWS_PROFILE=<profile> python3 scripts/build_virtual_icechunk.py \
--bucket schism-corie-output-097532040392 --tag <RUN_TAG from the sync log>
# Then explore it: notebooks/explore_icechunk.ipynb (xarray + xugrid + hvplot)- Provisions a single VM (
cpus: 40+, sized to match the reference run's 40 MPI ranks) on whichever of AWS/GCP is enabled and cheapest. Runs directly on the VM, not inside a container -- there's no isolation benefit to gain from containerizing a single-tenant HPC build, and it only adds a network hop that complicates instance-metadata (and thus credential) access (see the retired Docker-based approach, git history, for what that cost us). ~/.sky/config.yaml'saws.remote_identity(pattern-matched to theschism-coriecluster name) attaches a dedicated, narrowly-scoped IAM role (S3 write to exactly one bucket, nothing else) instead of SkyPilot's own shared default role -- see "One-time setup".resources.autostoptears the cluster down automatically after 30 idle minutes (no running job, no SSH session). This exists because a real run of this pipeline once sat idle for ~49 hours after finishing (~$99 in unnecessary compute) simply because nobody rememberedsky down.setup:installs build deps via apt, clones schism-dev/schism, buildspschismwith CMake using scribed I/O (-DOLDIO=OFF), and pulls theTest_CORIEtest case data viasvn export.run:insertsiof_ugrid = 2intoparam.nml(see "UGRID output" below), copies the built executable, optionally shortensrndayfor a smoke test, runsmpirun ... ./pschism <NSCRIBES>, and then syncsoutputs/to a per-run S3 prefix (timestamp + rnday, e.g.outputs/20260716T012233Z-rnday1/) using the instance's own role -- no separate upload step needed, and no risk of one run's sync overwriting another's (see the incident note under "The IAM role" below). Skips re-runningmpirunifoutputs/mirror.outalready shows a completed run, so re-invoking (e.g. after an interrupted sync) doesn't redo the compute.
These were found by actually running the pipeline, not just reading docs:
-DBLD_STANDALONE=ON -DBUILD_TOOLS=ONmust be passed explicitly. SCHISM's ownsrc/CMakeLists.txtcalls itsdefine_opt(name, doc, default)macro with arguments in the wrong order for these two flags (define_opt(BLD_STANDALONE "Standalone build mode" ON)— doc and default are swapped relative to the macro's(name, default, doc)signature). The result: both options silently cache asOFF, theDriver/subdirectory never gets configured, andmake pschismfails withNo rule to make target 'pschism'. Overriding both on the command line sidesteps the bug.- SCHISM's build tags the executable's filename with the enabled
options, e.g.
pschism_OLDIO_BLD_STANDALONE_BUILD_TOOLS_TVD-SB— the CMake target ispschism, but the binary is not.run:copies viapschism_*glob rather than a literal name. columbia.vims.edu's TLS cert is expired server-side (not a local CA-trust issue).svn exportneeds--trust-server-cert-failures=unknown-ca,expired,cn-mismatch,other.mpirunneeds--use-hwthread-cpuson 2-threads-per-core cloud instances (e.g.c6i.12xlarge= 24 physical cores / 48 vCPUs) — OpenMPI's default slot count is physical cores, so-np 40exceeds it without this flag. Unrelated to the container/VM question above -- this is purely about vCPU vs. physical core counts on the instance itself.- SCHISM writes its own log to
outputs/mirror.out, not stdout — check that file (not therun:block's captured stdout) for theRun completed successfullymessage. - Scribed I/O (
OLDIO=OFF) needs an explicit scribe-count argument:mpirun -np NPROC ./pschism NSCRIBES.NSCRIBESmust be at least (# enabled 3D outputs, with vector fields counting as 2) + 1 for the 2D bundle -- too few and the run errors out; extra scribes just sit idle.envs.NSCRIBESinschism-corie.yamlis set for Test_CORIE's defaultiof_hydroselections; changing which variables are enabled means recomputing this. - Test_CORIE's shipped
param.nmlpredates theiof_ugridflag entirely -- it's not in the&SCHOUTnamelist at all, commented or otherwise, sorun:appends the line rather than trying to toggle an existing one. - A bare
!in a double-quotedsedpattern triggers bash history expansion under SkyPilot's task runner (event not found), even though the script isn't interactive. Escape it as\!.
iof_ugrid is a param.nml flag, only meaningful under scribed I/O
(OLDIO=OFF): 0 disables UGRID metadata, 1 embeds full mesh data in every
3D output file, 2 (what this repo uses) has 3D files reference the mesh
data already written once in out2d_*.nc. With it set, out2d_*.nc carries
a proper cf_role="mesh_topology" variable, face/edge connectivity, and
Conventions: CF-1.12 -- verified directly against the actual output, not
assumed from the flag's docstring.
scripts/build_virtual_icechunk.py (run locally, not on the cluster --
see Quick start) groups a run's per-variable NetCDF files (out2d_*.nc,
temperature_*.nc, salinity_*.nc, etc. -- one file per variable per output
stack), virtualizes each group with VirtualiZarr
(byte-range references, no data copied out of S3), merges all variables into
one dataset, and commits it to an Icechunk repo at
s3://<bucket>/icechunk/<tag>/. Two things worth knowing if you touch this
script:
xr.concat's defaultdata_vars="all"stacks every data variable along the concat dimension, including static grid/connectivity arrays that don't vary between stacks and have no time dimension of their own -- giving them a spurious extra dimension thatxugridthen rejects. Fixed withdata_vars="minimal".- Neither
obstorenoricechunkreadAWS_PROFILEor a shared credentials file, and there's no EC2 instance-metadata endpoint to fall back to when running this locally (as opposed to on the cluster, where the scoped instance role Just Works). Credentials are resolved viaboto3.Sessionand passed through explicitly.
notebooks/explore_icechunk.ipynb opens that Icechunk store with
xr.open_zarr + xu.UgridDataset, and plots sample maps with
hvplot.xugrid. One extra step in there: Test_CORIE's hgrid.gr3 is
authored in Oregon State Plane North, NAD27, in meters (per its own
header) -- there's no separately-registered EPSG code for that exact
zone/datum in meters (the standard code, EPSG:32026, is in US survey feet).
Since PROJ's false easting/northing are always in meters regardless of
+units, swapping +units=us-ft -> +units=m in EPSG:32026's proj4 string
gives the same physical projection while matching the header. The notebook
uses xugrid's Ugrid2d.set_crs/.ugrid.to_crs() to actually reproject the
grid topology (not just relabel the coordinate arrays) to lon/lat, which is
what makes geo=True/basemap tiles work in the plots.
scripts/publish_public_icechunk.py --tag <TAG> (run with a profile that can
write to the target bucket) copies the run's NetCDFs to
s3://esip-qhub-public/rsignell/schism-corie/<TAG>/ and builds an anonymous
virtual Icechunk repo at .../<TAG>.icechunk (bucket is us-west-2). Versus the
private store it adds real SCHISM_hgrid_{node,edge,face}_{lon,lat} variables
(same Oregon North reprojection as above) and a (nface, 3)
SCHISM_hgrid_face_triangles array, and repoints the SCHISM_hgrid mesh
attributes at them -- gridlook needs degrees and 3-column triangle connectivity.
The bucket's CORS must allow gridlook's origin. gridlook link:
https://feat-ugrid.gridlook.pages.dev/#https://esip-qhub-public.s3.amazonaws.com/rsignell/schism-corie/20260717T143705Z-rnday3.icechunk::varname=temperature::dimIndices_time=0::dimIndices_nSCHISM_vgrid_layers=53
setup-runner-role.sh <bucket-name> is a one-time, idempotent step: it
creates a persistent schism-corie-runner IAM role (trust: ec2.amazonaws.com;
permissions: s3:PutObject/s3:ListBucket on exactly the bucket you name,
nothing else) and an instance profile wrapping it, and adds a
schism-corie: schism-corie-runner entry to ~/.sky/config.yaml's
aws.remote_identity list (pattern-matched by cluster name, so it only
affects clusters named schism-corie -- everything else keeps SkyPilot's
normal default role). This attaches the role to every launch automatically
-- no per-run credential setup, no swapping SkyPilot's own instance profile
back and forth.
teardown-runner-role.sh deletes the role, if you're done with this project
entirely. Day-to-day, you don't need it -- just sky down.
A gotcha worth knowing regardless of the above: SkyPilot's default
remote_identity behavior on AWS (when nothing overrides it) uploads your
entire local ~/.aws/credentials file -- every profile configured on your
machine, not just the active one -- to the cluster. Those static keys are
checked before any instance role in AWS's default credential chain -- if a
[default] profile with stale/invalid keys happens to exist there, it
silently shadows a perfectly good instance role, and every aws call on the
cluster fails with InvalidClientTokenId, which looks like a role/permissions
problem but isn't one. Naming a role explicitly via remote_identity (as
this repo does) avoids that upload entirely -- confirmed empirically:
ssh schism-corie "ls ~/.aws/" shows no such directory at all with this
setup, and aws sts get-caller-identity on the cluster correctly reports
the assumed schism-corie-runner role.
An incident this design fixes: an earlier version of this pipeline
synced every run to the same fixed outputs/ S3 prefix. Running a 1-day
smoke test after a real 28-day run had already been uploaded silently
overwrote that 28-day run's results -- same rank count means identical
filenames, and aws s3 sync has no collision protection and nothing was
using bucket versioning. The real 28-day output was unrecoverable. run:
now writes to a unique, timestamped prefix per invocation specifically to
make that impossible going forward.
A full 28-day run (rnday=28, dt=90, 26,880 steps) completed successfully
on AWS c6i.12xlarge in ~3.7 hours, matching the reference log
(mirror.out.ref) step count, with an empty fatal.error and physically
sane tidal elevations (final average |eta| ≈ 1.57 m). Total compute cost
for that validated pipeline (smoke test + full run) was under $15 -- a
separate, unrelated ~49-hour idle stretch afterward (now prevented by
resources.autostop) cost roughly $99 more and is not part of that figure.
Output (~59GB, OLDIO per-rank NetCDF + hotstart files) is not committed to
this repo; it lives in S3.
Note: that 28-day validation predates the switch to scribed I/O
(OLDIO=OFF) and UGRID output -- it was run under the now-retired OLDIO=ON
configuration. The current OLDIO=OFF/iof_ugrid=2 configuration has only
been validated at 3-day scale (rnday=3, 2,880 steps) so far: run completed
successfully, out2d_1.nc confirmed UGRID-compliant by direct inspection
(cf_role="mesh_topology", face/edge connectivity, Conventions: CF-1.12),
and the resulting Icechunk store round-tripped real data correctly (checked
actual elevation/salinity values through the virtual references, not just
structure). It has not yet been re-run at the full 28-day scale.