Skip to content

Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Files and Directories

hpc-compose writes to three independent on-disk roots, and keeping them separate is deliberate. Compose-level metadata lives next to the compose file so tracked records travel with your project; per-job runtime state lives under a per-job runtime root resolved at submit time; and the cache is a content-addressed store shared across jobs and visible from both the login node and the compute nodes. src/tracked_paths.rs is the single source of truth for every leaf name documented here, so the layout below matches what tooling reads and writes exactly.

The three roots at a glance

RootDefault locationSet withScopeHolds
Metadata directory<compose-file-dir>/.hpc-compose/(always next to the compose file)Per compose fileTracked job records, latest pointers, sweep manifests
Per-job runtime root<submit-dir>/.hpc-compose/<job-id>/x-slurm.runtime_rootPer jobLogs, metrics, artifacts, allocation files, state
Cache directory$HOME/.cache/hpc-compose/x-slurm.cache_dirShared across jobsContent-addressed images, enroot caches, rendezvous records

The metadata directory and the default per-job runtime root share the same .hpc-compose/ directory name, but they are addressed independently: the metadata root is anchored to the compose file’s directory, while the runtime root is anchored to the submit directory (and is overridable). They coincide only when you submit from the directory that holds the compose file and leave x-slurm.runtime_root unset.

Metadata directory

The metadata directory sits next to the compose file (metadata_root_for joins .hpc-compose onto the compose file’s parent). It holds the durable record of every submission plus the latest-pointers that let follow-up commands reconnect without resubmitting.

<compose-file-dir>/.hpc-compose/
├── latest.json              # most recent `up` (main) submission record
├── latest-run.json          # most recent `run` submission record
├── latest-canary.json       # most recent `germinate` canary record
├── latest-notebook.json     # most recent `notebook` server record
├── jobs/
│   └── <job-id>.json        # one tracked SubmissionRecord per submitted job
└── sweeps/
    ├── latest.json          # most recent sweep manifest pointer
    └── <sweep-id>/
        └── sweep.json       # per-sweep manifest
LeafKindContents
latest.jsonfileSubmissionRecord for the most recent up (main-kind) submission.
latest-run.jsonfileSubmissionRecord for the most recent run submission.
latest-canary.jsonfileSubmissionRecord for the most recent germinate canary submission.
latest-notebook.jsonfileSubmissionRecord for the most recent tracked notebook submission.
jobs/<job-id>.jsonfileThe authoritative SubmissionRecord for one job, keyed by Slurm job id.
sweeps/latest.jsonfilePointer to the most recent sweep manifest.
sweeps/<sweep-id>/sweep.jsonfileManifest describing one sweep and its trials.

A SubmissionRecord carries the paths the runtime root resolves to, including runtime_root (the resolved x-slurm.runtime_root override, present only when set), batch_log, batch_log_managed, and service_logs (the authoritative service-name to log-path map; see Log lifecycle). The current SubmissionRecord schema version is 3. Records written by schema 3 persist the runtime_root override when one was set; older records that lack the field fall back to the default <submit-dir>/.hpc-compose layout when read.

Per-job runtime root

Each job gets its own runtime root: <runtime-root>/<job-id>/, where <runtime-root> defaults to <submit-dir>/.hpc-compose (runtime_root_for) and is overridable with x-slurm.runtime_root (resolve_runtime_root). The renderer resolves this to an absolute path at submit time and bakes it into the rendered JOB_ROOT, so a running job never depends on $SLURM_SUBMIT_DIR being set or shared-visible at compute-node runtime. A relative x-slurm.runtime_root resolves against the submit directory; an absolute one is used as-is.

<runtime-root>/
├── logs/
│   ├── hpc-compose-%j.out        # default batch log (job-id, Slurm-expanded)
│   └── <service-token>.log       # one log per service (see Log lifecycle)
└── <job-id>/
    ├── state.json                # job state snapshot (latest view)
    ├── logs/
    │   └── <service-token>.log   # per-service logs, latest attempt
    ├── metrics/
    │   ├── meta.json
    │   ├── gpu.jsonl
    │   ├── gpu_processes.jsonl
    │   ├── slurm.jsonl
    │   ├── diagnostics/
    │   └── gpu-node-samples/
    ├── artifacts/
    │   ├── manifest.json
    │   └── payload/
    ├── allocation/
    │   ├── primary_node
    │   ├── nodes.txt
    │   ├── service-nodelists/
    │   ├── mpi-hostfiles/
    │   └── distributed-hostfiles/
    ├── service-exits/
    ├── hooks/
    └── attempts/                 # resume-aware runs only
        └── <n>/                  # logs/, metrics/, artifacts/, state.json per attempt
Leaf (under <job-id>/)KindContents
state.jsonfileLatest-view job state snapshot used by status and friends.
logs/<service-token>.logfileOne log per service for the latest attempt; the filename is encoded (see below).
metrics/meta.jsonfileMetrics collection metadata.
metrics/gpu.jsonlfilePer-sample GPU metrics.
metrics/gpu_processes.jsonlfilePer-sample GPU process attribution.
metrics/slurm.jsonlfileSlurm step statistics samples.
metrics/diagnostics/dirCollected diagnostic artifacts.
metrics/gpu-node-samples/dirPer-node GPU sample files.
artifacts/manifest.jsonfileManifest describing exported artifacts.
artifacts/payload/dirThe exported artifact payload tree.
allocation/primary_nodefileHostname of the primary allocation node.
allocation/nodes.txtfileThe full allocation node list.
allocation/service-nodelists/dirPer-service node lists.
allocation/mpi-hostfiles/dirGenerated MPI hostfiles.
allocation/distributed-hostfiles/dirGenerated distributed (torchrun-style) hostfiles.
service-exits/dirPer-service exit markers (<service>.jsonl).
hooks/dirMaterialized prologue/epilogue/event hook scripts and their manifest.
attempts/<n>/dirPer-attempt copies of logs/, metrics/, artifacts/, and state.json for resume-aware runs. These per-attempt state.json files are the data source for hpc-compose checkpoints attempt/requeue history.

The batch script keeps the root-level logs/, metrics/, artifacts/, and state.json as the “latest” view (it updates them to point at the most recent attempt) so status and export commands read the latest attempt without reconstructing shell logic.

Default batch log location

When you do not set x-slurm.output, real submissions get a baked --output directive at <runtime-root>/logs/hpc-compose-%j.out. Note that this parent is job-id-free (<runtime-root>/logs/, not under <runtime-root>/<job-id>/), because Slurm opens --output before the script body runs, so the CLI pre-creates that directory host-side before sbatch. The default basename deliberately avoids %x so a raw job name can never become a path component; %j is expanded by Slurm. Setting x-slurm.output replaces this default entirely. Dry-run previews (inspect, render) keep the portable Slurm default instead of a baked absolute path so committed example renders stay machine-independent.

Cache directory

The cache directory defaults to $HOME/.cache/hpc-compose/ and is set with x-slurm.cache_dir (resolved with the precedence documented in Spec Reference). It must be visible from both the login node and the compute nodes. Image artifacts are content-addressed: the filename embeds a short hash of the cache key, so identical inputs reuse the same artifact across jobs and machines.

<cache_dir>/
├── base/
│   ├── <hash>-<label>.sqsh        # imported base image
│   ├── <hash>-<label>.sqsh.json   # manifest sidecar
│   └── <hash>-<label>.sqsh.json.lock  # advisory-lock sidecar
├── prepared/
│   ├── <hash>-<name>.sqsh         # prepared runtime image
│   └── <hash>-<name>.sqsh.json    # manifest sidecar
├── enroot/                        # login-node shared enroot store
│   ├── cache/
│   ├── data/
│   └── tmp/
├── runtime/
│   └── <job-id>/                  # per-job compute-node enroot runtime cache
│       ├── cache/                 # ENROOT_CACHE_PATH
│       ├── data/                  # ENROOT_DATA_PATH
│       └── tmp/                   # ENROOT_TEMP_PATH
└── rendezvous/
    └── <name>/
        ├── latest.json            # current provider for this rendezvous name
        └── <token>.json           # historical per-registration records
LeafKindContents
base/<hash>-<label>.sqshfileA base image imported from a remote reference, named by <short-hash>-<label>.
base/<hash>-<label>.sqsh.jsonfileManifest tracking the cache entry.
base/<hash>-<label>.sqsh.json.lockfileAdvisory-lock sidecar that serializes concurrent manifest read-modify-write.
prepared/<hash>-<name>.sqshfileA prepared runtime image derived from a base image plus prepare steps, named by <short-hash>-<service-name>.
prepared/<hash>-<name>.sqsh.jsonfileManifest tracking the prepared entry.
enroot/cache/, enroot/data/, enroot/tmp/dirThe shared login-node enroot store used during host-side prepare. enroot/tmp is the default extraction scratch; redirect it to node-local storage with x-slurm.enroot_temp_dir (or cache.enroot_temp_dir / HPC_COMPOSE_ENROOT_TEMP_DIR) to avoid Stale file handle on shared filesystems.
runtime/<job-id>/{cache,data,tmp}/dirThe per-job compute-node enroot runtime cache; the renderer exports ENROOT_CACHE_PATH/ENROOT_DATA_PATH/ENROOT_TEMP_PATH at these paths (enroot_runtime_job_dir). Namespaced by job id so removing it never touches the shared cache root.
rendezvous/<name>/latest.jsonfileThe current provider record for one rendezvous name (atomic latest pointer).
rendezvous/<name>/<token>.jsonfileHistorical per-registration records, retained until TTL expiry or owner cleanup.

Manifest .lock sidecars carry no data and only serialize writers; the manifest JSON next to each artifact is the persisted record. See Connect Jobs Across Allocations for how rendezvous records are produced and resolved.

Repo staging vs cluster workspace provisioning

The three roots above are written by hpc-compose itself. They are not the same as the cluster workspaces and site storage directories your job reads and writes — those you provision yourself.

When you submit from a laptop with hpc-compose up --remote, the project is first staged to a per-project directory on the login node:

~/.hpc-compose-remote/<project>/      # rsync'd copy of your settings base on the login node

The staged root is the settings base: the directory that contains .hpc-compose/settings.toml. Keep that file at the repo root so your whole source tree is staged. If your compose file sits in a subdirectory with no repo-root settings file, only that subdirectory is staged and the rest of your tree is hidden from the job (hpc-compose warns when it stages only a subdir). The stage includes project settings (.hpc-compose/settings.toml, .hpc-compose/cluster.toml) but excludes tracked job/runtime state. See Submit From Your Laptop With up --remote.

Staging copies your repo. It does not allocate cluster workspaces (for example ws_allocate) or create site storage directories. You must create cache, dataset, checkpoint, and other site storage paths yourself before the run — a missing host bind-mount or storage directory blocks preflight.

Preflight remediation reflects this boundary. For a relative or in-repo missing path it tells you to create the directory; for an absolute missing path it notes that the path may be a cluster workspace or site storage location and should be provisioned with your site’s allocation command (for example ws_allocate) or an x-slurm.setup step, because hpc-compose stages your repo but does not allocate workspaces or create site storage directories.

Bootstrapping required directories

x-slurm.setup is the declarative bootstrap phase: its commands run on the allocated node before any service starts, so it is the right place to create the cache/data/results sub-directories your bind mounts expect. Allocate (or look up) the workspace first, then create the layout declaratively:

x-slurm:
  setup:
    # $WORKSPACE is resolved on the node (e.g. exported by an earlier step or your shell rc);
    # ws_allocate / ws_find belong in your session, not here, because they allocate quota.
    - mkdir -p "$WORKSPACE"/{cache,data,results,runtime}

For in-repo directories (relative bind-mount sources such as ./results), commit them with a .gitkeep so they exist and are staged, rather than relying on them being created at runtime. Use absolute cluster paths for large/scratch data that should not be staged, and relative in-repo paths for small inputs that travel with the project.

Excluding files from staging (.hpcignore)

A repo-root .hpcignore adds extra excludes on top of .gitignore when the source tree is snapshotted (for up, prepare, and up --remote). It uses gitignore-style patterns, so anchoring matters:

  • An unanchored directory pattern like data/ matches that name at any depth — including a Python package subtree such as src/mypackage/data/. Excluding package source there causes ModuleNotFoundError at runtime.
  • Anchor artifact patterns to the repo root with a leading slash — /data/, /runs/, /results/ — so they only match the top-level artifact directories and never a nested package.

hpc-compose warns when .hpcignore excludes any .py file (the usual symptom of this mistake). To see exactly what an .hpcignore removes from the snapshot, set HPC_COMPOSE_DEBUG_STAGING=1, which lists every excluded path during staging.

Environment variables that affect paths

hpc-compose both reads some path-affecting variables from your environment and sets others into the running job. The table below consolidates the relevant ones.

VariableDirectionEffect
HOMERead from environmentAnchors the default cache directory ($HOME/.cache/hpc-compose) when x-slurm.cache_dir is unset.
SLURM_SUBMIT_DIRRead from environmentNow only a preview fallback: dry-run renders use ${SLURM_SUBMIT_DIR:-$PWD}/.hpc-compose for JOB_ROOT. Real submissions bake an absolute runtime root, so the running job no longer depends on it.
SLURM_JOB_IDRead from environment (set by Slurm)Selects the per-job runtime root (JOB_ROOT/<job-id>) and the per-job enroot runtime dir (runtime/<job-id>); expanded into %j in the default batch log.
ENROOT_CACHE_PATHSet by hpc-composeExported to <cache_dir>/runtime/<job-id>/cache in the rendered batch script.
ENROOT_DATA_PATHSet by hpc-composeExported to <cache_dir>/runtime/<job-id>/data.
ENROOT_TEMP_PATHSet by hpc-composeExported to <cache_dir>/runtime/<job-id>/tmp at compute-node runtime; during prepare it defaults to <cache_dir>/enroot/tmp unless redirected (see HPC_COMPOSE_ENROOT_TEMP_DIR).
HPC_COMPOSE_ENROOT_TEMP_DIRRead from environmentOverrides the prepare-time enroot extraction scratch (default <cache_dir>/enroot/tmp). Mirrors x-slurm.enroot_temp_dir/cache.enroot_temp_dir; for up --remote prefer the spec or settings field, because a laptop env var does not propagate over SSH.
HPC_COMPOSE_PREPARE_GPURead from environmentOpts prepare-time image building back into enroot’s NVIDIA hook. Default is off: prepare runs CPU-only on the login node (NVIDIA_VISIBLE_DEVICES=void) so a CUDA image’s baked GPU request does not make the hook fail where no driver is present; GPUs are injected at Slurm/Pyxis runtime instead. Set to 1/true/yes/on only when the prepare host actually has a driver.
HPC_COMPOSE_BACKEND_OVERRIDERead from environmentSelects the runtime backend used by the batch script (defaults to slurm).
HPC_COMPOSE_DEV_CONTROL_DIRRead from environmentWhen set, enables the dev control directory used for live restart requests during local smoke-tests.
HPC_COMPOSE_DEBUG_STAGINGRead from environmentWhen truthy, lists every path excluded from the source snapshot by .hpcignore during staging (a staged-file manifest aid for debugging ignore rules).
HPC_COMPOSE_SERVICE_LOGSet by hpc-composePoints each service and its hooks at the in-container path of that service’s log file.
HPC_COMPOSE_RESUME_DIRSet by hpc-composeThe in-container path of the resume directory for resume-aware runs.

During login-node prepare the same enroot variables are pointed at the shared <cache_dir>/enroot/{cache,data,tmp} store rather than the per-job runtime/<job-id> store. The persistent layer cache (ENROOT_CACHE_PATH) always stays under cache_dir, but the temporary extraction scratch (ENROOT_TEMP_PATH) — and, when that scratch is redirected, the transient prepare rootfs (ENROOT_DATA_PATH, where enroot create unsquashes the image before the prepared .sqsh is exported) — can be moved to fast node-local storage together. By default the scratch stays at <cache_dir>/enroot/tmp; opt in by setting x-slurm.enroot_temp_dir in the spec (interpolation-aware, e.g. /tmp/${USER}-hpc-compose-enroot), cache.enroot_temp_dir in .hpc-compose/settings.toml (project-wide default, mirroring cache.dir), or the HPC_COMPOSE_ENROOT_TEMP_DIR environment variable. Precedence is HPC_COMPOSE_ENROOT_TEMP_DIR > x-slurm.enroot_temp_dir > cache.enroot_temp_dir > the <cache_dir>/enroot/tmp default. When the scratch is left at its default the prepare rootfs stays on the shared cache (<cache_dir>/enroot/data); redirecting the scratch moves both the extraction scratch and the transient rootfs to an hpc-compose-owned per-process subdir under the node-local path. This matters on shared NFS/Lustre/GPFS home/work storage, where the extract-then-mksquashfs import and the unsquashfs create step are slow and can fail with Stale file handle (ESTALE); pointing the scratch at node-local /tmp keeps the final .sqsh and layer cache on the shared cache while extraction and rootfs creation happen locally. The override applies to prepare-time import only, not the compute-node runtime. hpc-compose preflight surfaces the resolved enroot temp path, and hpc-compose context shows the settings-level value. The full set of HPC_COMPOSE_* runtime variables injected into services (distributed, rendezvous, MPI, scratch, and hook variables) is described in Monitor a Run and the feature guides.

Cleanup scope

Different commands reap different subsets of these roots. The table is precise about what each one deletes and what it leaves intact.

Command / mechanismDeletesPreserves
down (a.k.a. cancel)The job’s tracked record jobs/<job-id>.json, the per-job runtime root <runtime-root>/<job-id>/, the hpc-compose-managed default batch log when x-slurm.output was not set, the per-job enroot dir <cache_dir>/runtime/<job-id>/, and this job’s owned rendezvous records. Repairs the latest pointers afterward.Other jobs’ records and runtime roots, user-pinned x-slurm.output files, the shared cache root, base//prepared/ artifacts, and other jobs’ rendezvous records.
cleanThe same per-job state as down for each reaped record (tracked record, per-job runtime root, managed default batch log, per-job enroot dir, owned rendezvous records), selected by --age DAYS or --all (all except the latest).The retained records and their runtime roots, user-pinned x-slurm.output files, the shared cache root, and content-addressed artifacts.
Batch teardown trap (x-slurm.cleanup.runtime_cache)Only the per-job enroot runtime cache (ENROOT_CACHE_PATH/DATA_PATH/TEMP_PATH under runtime/<job-id>/), and only when the policy opts in. Default is never; on_success runs only on exit code 0; always runs on every clean exit.Everything else. Because cancelled or crashed jobs never run the trap, host-side down/clean are the reliable reapers of runtime/<job-id>.
cache prune (--age DAYS or --all-unused)Content-addressed artifacts (base/ and prepared/ entries plus their manifest/lock sidecars) that are expired or no longer referenced, and now-empty parent directories left behind.The cache root itself (never removed), still-referenced artifacts, and non-empty parent directories.
down --purge-cacheIn addition to the per-job teardown above, the cached artifacts attributed to this submission.The shared cache root and artifacts belonging to other jobs.
sweep cleanupTracked sweep trial records and per-trial runtime state, consistent with clean.The sweep manifest history under sweeps/ unless explicitly removed, and the cache.
rendezvous pruneExpired rendezvous records (latest and historical) across all names.Live latest.json pointers and other jobs’ unexpired records.

Two things to keep in mind: tracked metadata records live next to the compose file while the managed default batch log lives under <runtime-root>/logs/, so cleanup uses the persisted record to remove only the log hpc-compose owns; and the per-job enroot dir is namespaced by job id, so reaping it can never touch the shared cache root or another job’s runtime cache.

Log lifecycle

The default batch log (sbatch stdout/stderr) is <runtime-root>/logs/hpc-compose-%j.out unless you set x-slurm.output (see Default batch log location).

Service logs are written one-per-service under <job-id>/logs/. The filename is produced by a reversible token encoding of the service name: each non-alphanumeric byte becomes an _x{hh}_ hex sequence. For example, db.primary (the . is byte 0x2e) becomes db_x2e_primary.log. Do not parse these filenames by hand; the authoritative service-name to log-path map is SubmissionRecord.service_logs, which logs, watch, and replay read.

For resume-aware runs, each attempt’s logs and state are preserved under attempts/<n>/, while the root-level logs//state.json track the latest attempt.

Automatic size-based log rotation is not yet implemented. There is no x-slurm.logs key; cap log volume from inside your service command (for example by limiting verbosity or rotating within your own process) if a long-running service can produce unbounded output.