Node-local env staging: pack envs to single images, extract to local disk at spawn - #223
Open
OwenPriceSkelly wants to merge 3 commits into
Open
Node-local env staging: pack envs to single images, extract to local disk at spawn#223OwenPriceSkelly wants to merge 3 commits into
OwenPriceSkelly wants to merge 3 commits into
Conversation
…disk at spawn (#180) Cold worker starts on network filesystems are bounded by per-file metadata RPCs and mmap fault storms — the two costs the page-cache prewarm can't remove. This adds the structural fix from the prior-art survey: pack each built env into one tar.zst image, extract it to node-local disk at spawn, and run the worker entirely from the local copy. Pack half: - rootstock/pack.py: stream tar|zstd into {root}/images/<env>-<sha12>.tar.zst (env tree + its .python interpreter, root-relative), hashing in-flight; superseded images of the same env are removed. - Manifest schema v7: optional per-env `image` record (path, sha256, format, sizes, packed_at). Currency is `packed_at >= built_at`, stamped in the same refresh as built_at, so a rebuild without a repack reads as image-less. - `install` packs inside its own transaction (best-effort; `--no-pack` skips), so sync-triggered rebuilds get images for free. `rootstock pack` backfills. Stage half: - rootstock/stage.py: resolve the node-local base (ROOTSTOCK_STAGE_DIR > layout.json stage_dir > Cluster.stage_dir; ROOTSTOCK_NO_STAGE disables), validate it (exists, writable, different filesystem than the root), then extract content-addressed by archive sha with an O_EXCL lock (one extractor per node, losers wait for the atomic rename), disk preflight + LRU eviction, and venv fixups (bin/python* symlinks, pyvenv.cfg home) so nothing executes from the shared tree. - Checkpoint weights overlay into a local cache mirror from the manifest's recorded weight_files: recorded files are copied, everything else symlinks back to the shared cache so unrecorded side files (HF config.json etc.) still resolve; HF-hub snapshots/refs are recreated so their relative links land on the local blobs. Worker caches are repointed only when the overlay succeeds, and the prewarm is skipped only when nothing shared remains. - spawn_in_env: worker spawns stage; download spawns never do (they write the shared cache), and weights_capture runs keep caches shared so records can't be poisoned by mirror paths. Any missing piece falls back to the prewarm path — an unconfigured cluster behaves exactly as before. - CLI: `rootstock stage <checkpoint-id> ...` for job prologues (subsumes the #179 one-shot prewarm idea; degrades to a sequential prewarm pass where staging isn't configured), `rootstock init --stage-dir`, docs in cluster-setup.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…single manifest parse From the PR #223 review's verified follow-up findings: - `rootstock stage` now mirrors weights only alongside a staged env (spawns never consult a mirror without one), and its prewarm fallback goes through prewarm_from_spec — keeping the cgroup working-set warning and ROOTSTOCK_NO_PREWARM semantics — via a new `label` parameter instead of a hand-rolled copy. - Mirror currency is now size+mtime (copies preserve the source mtime), so a same-size in-place overwrite of a stable-path weight file invalidates warm mirrors instead of silently serving stale bytes. - Warm-mirror spawns are lock-free: a per-checkpoint completion marker records the digest of the last finished overlay (paths + source sizes/mtimes); matching it returns without the mirror lock or any shared-cache walk, so concurrent same-node spawns no longer serialize. The marker is written only after a full overlay, so a crashed pass is completed by the next spawn under the lock. - The weights free-space gate counts only bytes still needing copies — a warm mirror on a full disk stays staged. - One raw manifest read per spawn: stage_for_spawn reads the env record once and threads it through stage_env/stage_weights; the spawn-path prewarm lookup is skipped when the spawn is fully node-local (its result was dead payload under ROOTSTOCK_NO_PREWARM). - Cleanups: stage_env probes tools via pack_tools_missing(), image format checked against IMAGE_FORMAT, _fmt_bytes deduplicated into prewarm, dead StagedSpawn.weights_staged_bytes dropped, always-true partial-glob guard in pack removed, cluster-setup.md layout.json wording corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce + repair paths, prune images tier From the PR #223 review's main report, most-severe first: 1. Overlay: a fallthrough symlink is never treated as a current copy — stat follows the link to the very shared file, so a later-recorded weight file would have stayed on Lustre with the worker's prewarm off. 2. Eviction: staged envs carry .users/<pid> markers written by the client process; _evict_lru skips dirs with a live registered user, so a >6h MD run on persistent /tmp can't lose its env mid-trajectory. 3. {base}/rootstock is created sticky-1777 (the /tmp recipe) so the first user's umask can't lock everyone else out of staging on that node. 4. Pack partial sweep is pid+age aware (a concurrent pack's live partial survives), and a swept-partial rename failure surfaces as PackError instead of an uncaught traceback. 5. The bare `rootstock pack` sweep re-packs when the recorded archive file is missing on disk (purged images/ dir was previously unrepairable — "all current" forever). 6. pack_environments records successful packs in the manifest before raising on failures, so superseded-archive deletions can't strand the manifest pointing at deleted files. 7. Venv fixup remaps through install-time mount-alias spellings (/eagle vs /lus/eagle) by resolving the target against the resolved root; a deterministic fixup failure is noted per archive + client version so spawns stop re-paying extract-and-discard. 8. stage_env/stage_weights never raise (preamble mkdir/disk/lock failures degrade to prewarm with a log line, instead of crashing the prologue CLI), and _sweep_partials guards its stat against rename races. 9. Superseded-image cleanup matches <env>-<12 hex>.tar.zst exactly, so packing 'ani' can never delete 'ani-tuned-*' images. 10. prune gains an images GC tier: archives no manifest env records (and crashed packs' partials) are collected, age-guarded like the rest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
Author
|
Code-review follow-up status — all 16 verified findings (top-10 report + 6 overflow) are applied: overflow six in 128610d, top-10 in 8b5c7bc. Per-finding outcomes for the top-10, most-severe first:
Verification: 1010 tests passing (11 new), ruff/format/ty clean. 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #180. Subsumes #179 (the
rootstock stageCLI is the one-shot job-prologue warmer).Why
Cold worker starts on network filesystems (worst on Delta's HDD-backed Lustre, #160/#167) are bounded by two costs the page-cache prewarm (#168/#171/#178) structurally can't remove:
statof a 35k-file env tree is a round trip, andThe 2026-07-29/30 field data (3–4 concurrent prewarms at 946–1322 s/env; 8 of 45 sync verifies blowing the 600 s connect timeout on freshly rebuilt envs) motivated the prior-art survey, which landed squarely on this shape: single-image env + extract to node-local disk (CSC Tykky, NERSC Shifter, OLCF sbcast+NVMe, Meta XAR).
What
Pack —
installnow archives each env it builds into{root}/images/<env>-<sha12>.tar.zst(env tree + its.python/interpreter, root-relative paths, streamedtar | zstdwith in-flight hashing), inside the same transaction, best-effort (--no-packto skip; a failed pack degrades spawns to the prewarm path).rootstock packbackfills existing envs. Manifest schema bumps to v7 with an optional per-envimagerecord; currency ispacked_at >= built_at, stamped in the same refresh asbuilt_at, so a rebuild without a repack can never serve a stale image.Stage — at spawn, worker spawns resolve a node-local base (
ROOTSTOCK_STAGE_DIR>layout.jsonstage_dir>Cluster.stage_dirregistry fallback;ROOTSTOCK_NO_STAGE=1disables; the dir must exist, be writable, and be a different filesystem than the root), then:bin/python*symlink targets,pyvenv.cfg home =) so the interpreter and stdlib are local too — targets are written pre-rename in final-path terms, so the rename stays the atomic publication point;config.json— capture only sees mmap'd files) still resolve; HF-hubsnapshots//refs/are recreated so their relative links land on the local blobs;HOME/XDG_CACHE_HOME/HF_HOMEat the mirror only when the overlay succeeds, and setROOTSTOCK_NO_PREWARM=1only when nothing shared remains to warm.Safety boundaries: download spawns never stage (they must write the shared cache);
weights_capturespawns (add/verify) stage the env but keep caches shared, so captured records can't be poisoned with mirror paths — while first-verify-after-rebuild still gets the fast path that the 600 s timeout data asked for. Graceful degradation is load-bearing: any missing piece (no config, no image, stale image, no zstd, no space, lock timeout, any error) falls back to today's prewarm, and an unconfigured cluster behaves exactly as before.Config/CLI:
rootstock init --stage-dir '$SLURM_TMPDIR'writes the declaration tolayout.json(env vars expand on the node at spawn time);rootstock stage <checkpoint-id> ...warms envs+weights up front in job prologues and degrades to a sequential prewarm pass where staging isn't configured. Docs incluster-setup.md.Testing
35 new stage/pack tests (real tar|zstd round trips, venv fixups, warm reuse, content-addressed rebuild-beside, every fallback edge, the HF-hub overlay indirection, spawn integration gates) plus manifest v7 migration/round-trip/currency coverage. Full suite: 997 passed; ruff/format/ty clean.
Follow-ups (not in this PR)
prune(an env pruned after packing leaves its image behind; repacks already clean their own superseded images).stage_dirrecon + rollout, Delta first (node-local path + wipe policy still unconfirmed there), then A/B against the prewarm path withROOTSTOCK_NO_STAGE— the metric is the multi-worker congested-afternoon case.🤖 Generated with Claude Code