Make lc seamless on NERSC Perlmutter - #194
Open
EiffL wants to merge 4 commits into
Open
Conversation
The Perlmutter spike (#192) proved both materialize modes work multi-node, then found four things that made real use rough. Each is fixed here and verified by running it on the machine. The site registry supplies uv's plumbing. On a known center, unset UV_CACHE_DIR and UV_PYTHON_INSTALL_DIR are filled from $SCRATCH/.lightcone/ — spec §4's row, made real. Both move state off a home filesystem that measurement found unusable from compute nodes, in two independent ways: flock fails there outright (os error 524), which killed every uv-touching verb, and an interpreter stored there starts in 0.4–9 s rather than 0.05 s, enough under load to exceed uv's own 60 s ceiling and fail a sync mid-run. Per variable, ambient always wins; silent by design, and deterministic in the ambient environment, so every node of an allocation derives the same answer with nothing handed down. MOUNT_* is scrubbed; ENABLE_* is kept. podman-hpc's site modules are env-gated, and the mount gates bind $HOME/$SCRATCH/$CFS into every recipe container — undeclared inputs under a manifest attesting fs: declared. The library gates stay: CUDA and MPI system libraries are the /dev,/sys generosity class, and the site's own GPU mechanism has to keep working. One predicate in the one env composer covers both wrapper-spawn paths; uv_scrub_warning becomes scrub_warning and names both families. A stale squashed image is healed before the load. The tag is deterministic and builds are not bit-reproducible, so a rebuild migrated under an unchanged tag put a second same-named image into podman-hpc's read-only squash store, after which every storage operation failed. An unrecognized probe outcome is left for migrate's own loud path, so the heal cannot break a healthy run. The driver releases each future as its result is consumed. A key still client-held at teardown makes retire_workers replicate it onto peers that are also retiring, so the run stalled past the reap grace and srun was killed: every clean SLURM run ended in "srun: forcing job termination". Warnings on an unclean path still fire. Also: the containerized sync creates uv's cache directory before bind-mounting it (nothing on the host makes it in that mode, so podman died with a raw statfs error about a path the user never named), and the Landlock downgrade note names the boot-time LSM list first — Perlmutter is kernel 6.4 with Landlock simply absent from it. Verified on a 2-node interactive allocation: a fresh project with an empty environment goes from lc init to a committed result with no setup; direct and containerized multi-node runs spread across both nodes and end silently; a manufactured duplicate-name wedge healed and ran clean. Full suite on a compute node: 568 passed, 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyNDEbtjpob2CJSQMnEiuf
Contributor
✅ Eval
lc statusConfusion & pain points (Claude analysis)Confusion & pain points
Full trace: |
Review findings from the NERSC work, both verified against the real podman-hpc on Perlmutter. Site modules are named in the attestation. The MOUNT_*/ENABLE_* split kept ENABLE_* on the stated ground that those modules bind system libraries only, "never a channel undeclared inputs arrive through" — measurably false: ENABLE_CVMFS binds the whole populated /cvmfs hierarchy, which is reference data a recipe can read without declaring it, and ENABLE_MPICH_SS adds --privileged plus the host's network, pid and ipc namespaces, after which the mount table is not the boundary at all. Only ENABLE_GPU and ENABLE_NCCL* fit the original description. They stay working — they are how a GPU or MPI recipe reaches the hardware it was written for — so the honesty comes from the record: container.site_modules() names the gates that are set, OCIBackend carries them as a resolved field (the user_flags pattern, so wrap stays pure), and Attestation.site_modules puts them in every manifest. It is the one attestation value not derived from lc's own argv, because the runtime applies them from its own environment, and the docstring says so. A per-module allowlist was rejected: the module table is site-configurable, so a center adding a data-binding module lc has never heard of would slip through a hardcoded judgement. The squash heal removes by id. `rmsqi <tag>` resolves a single record (remove_image → get_img_info takes the first name match), so against the two-same-name state being healed it could take the current image and leave the stale one to re-wedge the store on the next migrate. Each stale id is now removed by its own call; only the wedged branch spells the tag, because a store that cannot be listed cannot be enumerated, and it re-probes bounded by _HEAL_ATTEMPTS since each blind pass is a real deletion. Also: creating uv's cache directory raises ProjectError rather than a bare OSError, which no caller translates — a full quota or an unwritable UV_CACHE_DIR would have surfaced as a traceback out of lc materialize instead of a refusal naming the path. Recorded rather than fixed: the heal can drop layers a concurrently running allocation is still executing from (leaving the store wedged breaks that run too, and cross-run coordination is not something a heal can offer). Verified: a real containerized run with ENABLE_CVMFS=1 ENABLE_GPU=1 records both in hermeticity.site_modules, and `lc run ls /cvmfs` does list the repositories — the widening is real and the manifest now says so. 573 passed on a compute node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyNDEbtjpob2CJSQMnEiuf
CI caught the attestation naming a module the host does not have: matching `ENABLE_*` picked up GitHub's own ENABLE_RUNNER_TRACING, so a manifest would have recorded a site module that was never applied — the exact dishonesty `site_modules` exists to prevent, pointed the other way. The gates are asked of the mechanism instead of guessed. Each module in podman-hpc's table declares its own `env:` key, and `podman-hpc infohpc` reports where the table lives (a site can move it), so lc reads the real gate names and keeps the ones the environment sets. That is the same probe-don't-assume rule `git check-attr` and `uv cache dir` already follow. A table that cannot be read reports nothing rather than refusing, the uv_version discipline: attestation must not fail a run. Verified against the live table on Perlmutter — ENABLE_CVMFS and ENABLE_GPU recorded, ENABLE_RUNNER_TRACING ignored, MOUNT_HOME absent because child_env scrubbed it. The tests now stub a module table of their own, so they no longer depend on the host's environment: that dependency is why this passed locally and failed on all four CI legs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaQvhUZHHrGCMZxKLdAfgR
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 the gaps the Perlmutter spike found (#192), so a researcher on NERSC can
lc initandlc materializewith an empty environment and nothing to configure. Every change here was verified by running it on Perlmutter, on a 2-node interactive allocation.The problems, and the fixes
uv's home-filesystem state is unusable from compute nodes — in two independent ways.
flockon$HOMEfails outright (os error 524), so every uv-touching verb died until the user exportedUV_CACHE_DIR. And an interpreter stored there starts slowly enough to blow uv's own 60 s ceiling and fail a sync mid-run withPython startup timed out. Measured on the same compute node, 5 runs each:python -c pass$SCRATCH(Lustre)$HOME(GPFS)So
venue.site_env()supplies bothUV_CACHE_DIRandUV_PYTHON_INSTALL_DIRfrom$SCRATCH/.lightcone/on a known center — spec §4's "the site registry suppliesUV_CACHE_DIRon Perlmutter" row, finally made real, and_UV_KEPTalready kept both variables for exactly this story. Per variable, ambient always wins. Silent by design: a default doing its job is not a warning. Deterministic in the ambient environment, so every node of an allocation derives the same answer with nothing handed down.An ambient
MOUNT_HOME=1silently bind-mounted$HOMEinto every recipe container while the manifest attestedfs: declared. podman-hpc's site modules are env-gated, sochild_envnow scrubsMOUNT_*.ENABLE_*is deliberately kept — those bind CUDA and MPI system libraries, the same generosity class as the/dev,/sysgrants, and a GPU recipe on Perlmutter reaches CUDA through exactly that mechanism. One predicate in the one env composer covers both wrapper-spawn paths, so noBackendprotocol change was needed.uv_scrub_warning→scrub_warning, naming both families.Rebuilding an image under its own tag wedged the shared squash store. The tag is deterministic but builds are not bit-reproducible, so a rebuild migrated under an unchanged tag put a second same-named image into podman-hpc's read-only store — after which every storage operation failed, runs included.
container._heal_squashprobes andrmsqis the stale copy before the load (a wedged store fails the load too). An unrecognized probe outcome is left for migrate's own loud path, so the heal can never break a healthy run.Every clean SLURM run ended looking like a crash —
srun: forcing job termination,task N: Killed, and scheduler lose-tasks warnings. Cause: the driver held every future for the run's lifetime, soretire_workerssaw each key as client-wanted and tried to replicate it onto peers that were also retiring, stalling past the reap grace until srun was killed._Dask.completednow releases each future as its result lands. Warnings on an unclean path still fire, honestly.Also: the containerized sync now creates uv's cache directory before bind-mounting it — in that mode uv never runs on the host, so nothing else makes it and podman died with a raw
statfserror about a path the user never named (latent before; the cache supply made it reachable). And the Landlock downgrade note names the boot-time LSM list first: Perlmutter is kernel 6.4 with Landlock simply absent from that list, so the old text offered three causes and none of them applied.Verification
Live, on
nid[004167-004168]:lc initon a compute node, empty environment$SCRATCH/.lightcone/mechanism: podman-hpc,fs: declared, image id pinnedMOUNT_HOME=1 lc run ls $HOMEpodman-hpcwith the same variable does mount itlc init→lc materialize→ committed result in 61 s, clean treeThe future-release fix also removed a suite flake and tripled suite speed on a compute node — same suite, same node, only the interpreter store moved: 9 failed / 42:16 → 568 passed, 0 failed / 13:45.
Notes for reviewers
conftestscrubsNERSC_HOSTsuite-wide (deliberately — the suite must exercise the non-site path), so the tests do not get the interpreter fix automatically. Anyone running pytest on a NERSC compute node shouldexport UV_PYTHON_INSTALL_DIR=$SCRATCH/.lightcone/uv-pythonor expect the flake; real users get it fromchild_env.projectnow importsvenuelazily insidechild_env(venue importsProjectErrorat module scope). The alternatives each broke a recorded invariant — moving the site table would contradict "supporting another center is one row and nothing else moves", and merging at call sites would spread env composition to three places.--ignore=tests/test_container_smoke.pysilently does nothing in this repo (testpathswins);--deselectis the spelling that works._inspect_idKeyError: 'podman-hpc'and the bare-podman-on-network-filesystem failures (both from Perlmutter spike: login-node findings (layer 7) #192's first half), andOMP_NUM_THREADS, which is documented rather than touched —lcdoes not take over a variable it does not own.Docs land with the code: a "NERSC (Perlmutter) specifics" section in the cluster guide, the duplicate-squash entry in troubleshooting, and four API pages. CLAUDE.md carries the decisions.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DyNDEbtjpob2CJSQMnEiuf