Setting up a new cluster¶
This guide is for administrators setting up Rootstock on a new cluster. Run all commands below on the cluster itself after SSH access is established. Write access to a shared filesystem location visible to users is required.
Prerequisites¶
- SSH access to the cluster
- Write access to a shared filesystem location
- Python 3.11 or later
uvpackage manager (Rootstock uses it internally)
Step 1: Install Rootstock¶
On a login node:
Step 2: Initialize the Rootstock directory¶
Choose a location on a shared filesystem where other users have access:
Install root vs. cache root¶
On most clusters a single shared filesystem hosts both the rootstock install (code, venvs, manifest) and the model-weight cache. Some clusters require these to live on different filesystems — typically because the recommended persistent project filesystem doesn't support flock, which the HuggingFace cache requires. NERSC Perlmutter is one such case: code lives on CFS, model weights on PSCRATCH.
The install declares its own cache root in {root}/layout.json. Every reader (CLI commands and RootstockCalculator, whether given cluster= or root=) resolves the cache root the same way: an explicit override wins, then the install's declaration, then — for legacy installs that predate the declaration — the cluster registry's entry, then the install root itself.
Setting it at install time. rootstock init asks:
Model weights can live on a different filesystem than the install
itself — some clusters require it (the project filesystem may not
support flock, or weights may belong on scratch).
Cache on a different filesystem? (y/n) [y]:
Cache root [/pscratch/sd/u/me/rootstock-cache]:
The suggestion is seeded from the cluster registry when the root belongs to a known cluster, but you can override it — don't accept a path just because it was pre-filled, since it reflects whatever registry this client release happened to ship. Pass --cache-root to skip the prompt entirely:
Verify what an install actually declares with:
Changing it afterwards is deliberately awkward. Only init sets the cache root; rootstock install records the declaration when one is missing but never changes an existing one, so no routine rebuild can re-point a deployment and scatter checkpoints across two filesystems. To move a populated install's cache: edit cache_root in {root}/layout.json, then move the cache/ and home/ directories to the new path (or re-run rootstock add to re-download into it). Don't re-run rootstock init on a populated root for this — it writes a fresh manifest and loses recorded environment state.
The cluster registry (rootstock/clusters.py) is only a name → install-path bootstrap so users can say cluster="perlmutter" instead of remembering a path. Its per-cluster cache_root field remains solely as the legacy fallback above; new split-filesystem deployments don't need a registry entry at all, just a declaration in the install:
"perlmutter": Cluster(
root=Path("/global/cfs/cdirs/m5268/rootstock"),
cache_root=Path("/pscratch/sd/o/oprice/rootstock-cache"), # legacy fallback only
),
Users don't need to set environment variables — RootstockCalculator(cluster="perlmutter", ...) resolves both automatically.
Trust model¶
Using a shared install means trusting its maintainer. An environment's
setup() function is maintainer-authored Python executed with the calling
user's credentials — inside the worker subprocess, as you, with your
filesystem access and your HF tokens (forwarded so gated models work).
Rootstock isolates dependencies, not privilege: the pre-built venv keeps
MLIP stacks out of your Python environment, but it is not a sandbox.
Concretely: only use installs whose maintainer you trust, exactly as you
would a module farm or a shared conda env maintained by your facility. The
permission recipe below makes installs world-readable, and only
maintainers can modify env_source.py — so the code you run is the code the
maintainer published, but what that code does runs as you.
Permissions for shared installs¶
Shared installs on HPC clusters should be world-readable: anyone on the cluster (not just members of the maintainer's project group) should be able to use the environments and model weights. Nothing in a rootstock install is sensitive — it's all derived from public PyPI packages and public model checkpoints. Maintainer secrets (API tokens) live in the maintainer's ~/.config/rootstock/config.toml, not in the shared root.
The setup needs to satisfy:
- Maintainers (and only maintainers) can write —
rootstock install/rootstock addsucceeds for them. - Project group members inherit write via the maintainer's group, so a co-maintainer in the same project can take over without re-doing perms.
- All other cluster users get read + traverse.
- New files created by
uv pip install(and by rootstock) inherit the project group, world-read, and group-write, so the above stays true going forward.
rootstock setup-perms renders and applies this recipe for you. Run it once as the maintainer, before rootstock init. Pass a registered cluster name (it resolves both the install root and, where split, the cache root) and your project group:
Or point it at explicit paths instead of a cluster:
rootstock setup-perms /path/to/install/root \
--cache-root /path/to/cache/root \
--group <group> --apply
Omit --apply for a dry run (the default) that just prints the chmod / chgrp / setfacl commands — useful if you (or a cautious sysadmin) want to review them or paste them into a script before anything touches the filesystem. --apply runs them after a confirmation prompt, stopping at the first failure, then re-runs the read-only check and reports anything the filesystem didn't actually honour.
The order matters: the chmod comes last. Setting an ACL rewrites a path's mode bits, and on some filesystems (observed on NERSC CFS) that clears the setgid bit — so the mode is asserted after all the setfacl work, not before. If you hand-roll the recipe, keep that order.
If the install or cache root already has files in it when you set this up (e.g., you're retrofitting a deployment that started out project-only), add --retrofit so the recipe also applies recursively — existing files become world-readable, and existing subdirectories get setgid so files created under them inherit the project group:
rootstock install re-checks these permissions up front on every run and prints a warning if the root doesn't look world-readable (wrong mode bits, missing setgid, missing default ACL, or a mask clamp from too-restrictive a umask). The check is read-only and never blocks the build; pass --no-perm-check to silence it.
rootstock install and rootstock add force umask 002 for their own duration (uv subprocesses inherit it), so files they create are born world-readable and group-writable regardless of your personal umask. For anything you write into the shared root by hand (e.g. dropping an env file into environments/), your shell still needs umask 002. Add to ~/.bashrc (or whatever rc the cluster sources for non-interactive shells):
On clusters with split filesystems (cache root on a different mount than the install root), set:
uv defaults to hardlinking from its cache into target venvs, which fails across filesystem boundaries and falls back to copy with a noisy warning. Setting copy mode silences the warning and is the correct mode for cross-filesystem builds anyway.
After rootstock init runs, verify ACLs landed correctly with getfacl on a freshly created file. Group should show rwx (effective rw- or rwx) and mask::rw- or stronger. If you see mask::--- or #effective:---, the maintainer's umask was too restrictive when the file was created — rerun with umask 002 and rewrite the file.
The usage-record spool¶
setup-perms also creates {cache_root}/usage/ with mode 1777 (sticky, /tmp-style). This is the spool for anonymous per-session usage records: any user's session must be able to drop a record file, and the sticky bit keeps users from deleting each other's. Records are created O_EXCL with unique names, so the directory behaves like /tmp, not like a writable code path — but some centers' security scans flag any world-writable directory in project space. If that's a problem on your cluster (or you just don't want usage collection), pass --no-usage-spool: a missing spool means collection is off for the install, and check-perms treats its absence as a deliberate choice, not an issue. Aggregate with rootstock usage report, fold raw records into monthly rollups with rootstock usage compact, and send the aggregated counts to the dashboard with rootstock usage push (uses the same API credentials as manifest push; all three are safe to run from the same cron as the nightly smoke-test).
On clusters where your write access to the install is granted temporarily and later revoked (e.g. Delta), don't put the real spool inside the install — you'd eventually lose the ability to fix its permissions or prune it. Pass --usage-dir <path> pointing somewhere you permanently control (a directory under your home works): setup-perms creates the 1777 directory there and symlinks {cache_root}/usage to it, so sessions and the collector follow the link without any configuration. Make sure the target's ancestors grant world traverse (chmod o+x ~ if the spool lives under your home). If the spool ever does become unwritable, user calculations are unaffected — sessions silently skip recording — and check-perms will tell you.
Verifying world-readability before launch¶
Per-cluster step-by-step runbooks live in scripts/runbooks/ — they sequence the checks below and record which findings are known-benign. The pieces:
Quick check (first line). rootstock check-perms runs the same read-only verification that rootstock install performs up front, as a standalone command — plus an ancestor walk, so a restricted project parent directory (which no chmod inside the install can fix) shows up too. It stats only the roots and their ancestors, so it is safe and fast on login nodes:
Exit code 0 means the roots look right; 1 means issues were printed (pass --json for machine-readable output). It checks only the root directories, not the tree beneath them — for that, use the audit below.
Static audit (full tree). scripts/check_world_readable.sh checks the world-readable contract across the whole tree without needing rootstock in the caller's Python:
It walks the ancestor directories (every one needs o+x — if the project parent on CFS lacks it, that's a facilities ticket, not a chmod), checks other-bits on every file and directory, resolves symlink targets, and scans for per-user ACL entries and mask clamps that ls -l won't show. It prints actionable per-path fixes and exits nonzero on any violation.
Functional test (the ground truth). Static checks can't prove the contract for the person who matters: have someone who does not own the install and is not in the project group load a model end-to-end through RootstockCalculator. The account matters, and there is no way to fake it: owner bits mask everything for whoever built the tree, group bits/ACLs mask everything for project-group members, and user namespaces (unshare) hide group membership without dropping it.
Initial setup¶
Run the initialization command:
This will interactively prompt you for:
| Setting | Description |
|---|---|
| root | The shared directory path, or a registered cluster name (perlmutter, delta`, etc.) |
| api_key / api_secret | Optional credentials for pushing the cluster manifest to the dashboard |
| maintainer name / email | Identifies the maintainer for this installation |
Dashboard Integration
If you provide API credentials, Rootstock pushes the cluster manifest to the dashboard automatically whenever you install or update environments. You can skip this and run a deployment that's never published.
Step 3: Install environments¶
Still on the login node — install only builds the venv, no model weights yet:
# Install individual environments
rootstock install mace.py
rootstock install uma.py
rootstock install tensornet.py
# Or point it at a directory with multiple environments
rootstock install ./environments/
# Verify everything is set up
rootstock status
Each rootstock install command:
- Creates an isolated virtual environment under
{root}/envs/ - Installs MLIP dependencies
This process can take several minutes per environment, depending on the MLIP and network conditions.
Step 4: Add checkpoints¶
rootstock add is a separate, idempotent step that downloads weights and (where available) verifies them with a forward pass. Splitting download from verify lets you do the right thing on each kind of node:
# Login node (CPU, has network): download weights only
rootstock add mace-mp-0-medium --no-verify
rootstock add uma-s-1p1 --no-verify --kwarg task=omat
# GPU node (no network): skip download (already fetched), verify on GPU
rootstock add mace-mp-0-medium
rootstock add uma-s-1p1 --kwarg task=omat
If a node has both network access and a GPU, run without --no-verify to do everything in one shot.
rootstock add is idempotent — re-running it after a successful download will skip the download phase and just re-verify.
Non-CUDA accelerators. --device defaults to cuda; pass the right one for the cluster (e.g. --device xpu on Intel/Aurora). On Aurora, verify on a compute node with one PVC tile pinned (export ZE_AFFINITY_MASK=0), and give large models extra time — the Intel GPU runtime compiles kernels on the first load, so UMA needs a raised smoke-test --verify-timeout (e.g. 1800). Install the envs from aurora_configs/ (they pin the XPU torch wheel index).
rootstock smoke-test re-verifies every fetched checkpoint. To keep a cluster's
manifest current automatically, set it up on a schedule — see
Nightly Automated Smoke-Testing for the recommended
approach (a self-scheduling GPU batch job, with ready-to-edit SLURM and PBS
recipes). It needs no cron and no off-cluster pieces, and is portable across sites.
Smoke-test always uses default kwargs
smoke-test calls each env's setup() with no extra kwargs. A checkpoint that only works with non-default kwargs (e.g., a UMA checkpoint that needs task=omol) will appear failing in nightly smoke-test even though add succeeded. The remedy is to make the preferred kwargs the env's default in the env file.
Finding Environment Files
The live dashboard manifest at garden-ai-prod--rootstock-admin-dashboard.modal.run exposes the environment source file for every deployed env. Copy a working source as a starting point for your cluster — some tweaks may be required for site-specific requirements.
Step 5: Register with the dashboard (optional)¶
If you configured API credentials during rootstock init, the manifest is pushed automatically when you install or update environments.
Managing the manifest¶
The manifest tracks the state of your Rootstock installation and is used by the dashboard to display available environments. You can manage it with the following commands:
View current manifest¶
# Display the manifest in human-readable format
rootstock manifest show
# Output as JSON
rootstock manifest show --json
Push manifest to dashboard¶
If the automatic push failed (e.g., due to network issues), you can manually retry:
Initialize a new manifest¶
To create or reinitialize a manifest for a cluster:
# Create a new manifest
rootstock manifest init --cluster delta
# Overwrite existing manifest
rootstock manifest init --cluster delta --force
# Skip automatic push to backend
rootstock manifest init --cluster delta --no-push
Shared installs (one root, several clusters)¶
When several machines mount the same install (sophia and polaris share one
Eagle root), the install keeps one manifest listing every cluster it
serves — --cluster is repeatable, and registry siblings of the root are
included automatically:
Build and fetch state is shared (one filesystem), but verification is
per-cluster: passing on sophia says nothing about polaris. Commands that
record verification (add, smoke-test, sync) therefore need to know
which machine they are on — pass --cluster <name>; single-cluster installs
need no flag. Pushes send one manifest per cluster to the dashboard, each
carrying that machine's own verification results.
Verifying the installation¶
After setup, verify that everything works:
Directory structure¶
After setup, the Rootstock root directory will look like this:
{root}/
├── layout.json # on-disk layout version (future clients check this)
├── .python/ # uv-managed Python interpreters
├── environments/ # Environment source files (*.py with PEP 723 metadata)
│ ├── mace.py
│ ├── mace.py.lock # uv lockfile — rebuilds resolve from this
│ ├── uma.py
│ ├── uma.py.lock
│ └── tensornet.py
├── envs/ # Pre-built virtual environments
│ ├── mace/
│ │ ├── bin/python
│ │ ├── lib/python3.11/site-packages/
│ │ ├── env_source.py
│ │ └── env_source.py.lock # what this build was resolved from
│ └── ...
├── wheels/ # Vendored rootstock wheels (rebuilds install from here,
│ # so they don't depend on PyPI still serving the release)
├── home/ # Redirected HOME for not-well-behaved libraries
│ ├── .cache/fairchem/
│ └── .matgl/
└── cache/ # XDG_CACHE_HOME and HF_HOME for well-behaved libraries
├── mace/
└── huggingface/
Why the home/ directory?¶
Some ML libraries (FAIRChem, MatGL) ignore XDG_CACHE_HOME and write to ~/.cache/ unconditionally. Rootstock redirects HOME during environment builds and worker runtime so model weights land in the shared install directory rather than in individual users' home directories.
Updating environments¶
Rebuilds honor the env's lockfile by default: rootstock install mace --force reproduces the dependency stack that was already qualified on the cluster (this is the safe way to roll out an env-source fix). To deliberately move to newer packages, re-resolve with --upgrade:
# Rebuild the venv (drops verification timestamps for that env's checkpoints).
# Add --upgrade to re-resolve dependencies to the latest allowed versions.
rootstock install mace.py --force
# Re-verify checkpoints after the rebuild, by canonical id
rootstock add mace-mp-0-small
rootstock add mace-mp-0-medium
rootstock add mace-mp-0-large
# Or re-verify every fetched checkpoint at once
rootstock smoke-test
# Push updated manifest
rootstock manifest push
Rebuilding an env invalidates prior verifications (the venv changed; weights in cache/ are unaffected). rootstock status will show those checkpoints as stale until you re-run add or smoke-test.
Rebuilds are safe on a live shared install: the new env is built in {root}/.build/ and swapped into envs/ only when finished, so users can keep spawning workers from the old env for the whole build, and a failed rebuild leaves the old env untouched and working.
Hotfixing setup() without a rebuild¶
envs/<name>/env_source.py is re-read at runtime on both sides of the socket: every worker spawn imports setup() from it, and every client resolution AST-parses its CHECKPOINTS table. Nothing caches it across runs — which means a maintainer can fix a bug in setup() (or adjust a CHECKPOINTS entry) by editing that file in place on the shared filesystem, with no rebuild. The next worker spawn picks it up. This is the one cheap lever into already-built envs, and it is a supported procedure:
- Edit a copy, then move it into place (an in-progress edit with a syntax error breaks every new worker spawn on the cluster):
- Apply the same edit to the registered source
{root}/environments/mace.py— that file is what rebuilds build from, so skipping this step means the nextinstall --forcesilently reverts your hotfix. - Verify and refresh the manifest:
rootstock smoke-test --env mace(orrootstock add <id>) exercises the fixedsetup()and pushes an updated manifest. Until this runs, the manifest'ssource_hashfor the env is stale — it still hashes the pre-hotfix source. - Contribute the fix back to the sample env file in the repo so the next cluster doesn't need the same hotfix.
What a hotfix can and cannot reach. In-place edits can change anything setup() does (model loading logic, upstream URLs/paths, CHECKPOINTS values, new optional kwargs) — but they cannot change the env's dependencies (the venv is already built; dependency changes need install --force) and cannot fix worker/protocol code (that's the rootstock pinned inside the venv; only a rebuild replaces it).
Troubleshooting¶
Environment build fails¶
Check that you have:
- Sufficient disk space in
{root}/ - Network access for downloading packages and model weights
- Correct Python version (3.10+)
Users can't access environments¶
Verify permissions:
# Environments should be readable by all users
ls -la {root}/envs/
# Model weights in cache should also be readable
ls -la {root}/cache/
Dashboard push fails¶
Check your API credentials and network connectivity, then retry the push: