Skip to content

Non-agentic evals: parameterize evalchemy sbatch paths by $USER - #85

Open
mrinal-bespoke wants to merge 5 commits into
open-thoughts:mainfrom
mrinal-bespoke:pr/a-evalchemy-user-paths
Open

Non-agentic evals: parameterize evalchemy sbatch paths by $USER#85
mrinal-bespoke wants to merge 5 commits into
open-thoughts:mainfrom
mrinal-bespoke:pr/a-evalchemy-user-paths

Conversation

@mrinal-bespoke

Copy link
Copy Markdown

Problem

The four evalchemy sbatch scripts hardcode /leonardo_work/AIFAC_5C0_290/bfeuer00/...
for every path — conda root, the evalchemy-marin clone, nltk data, HF cache, JIT
caches, and the output root. A second operator cannot run a non-agentic eval at all.

Change

Split paths by whether they are read-only shared runtime or per-user writable:

  • Read-only runtime (conda env, evalchemy-marin clone, nltk data) — use the
    submitter's own copy if present, else fall back to the shared bfeuer00 one. A new
    operator does not have to duplicate a ~20 GB conda env to run an eval.
  • Writable (HF_HOME/HF_HUB_CACHE, CACHE_ROOT, OUT_ROOT) — always per-user.
  • #SBATCH --output uses SLURM's %u.

Each is ${VAR:-<default>}, so an explicit env var still wins.

This is a no-op for the original operator: with USER=bfeuer00 every path resolves
to the same byte-identical string as before.

Notable fixes

Conda fallback must validate the env, not the root. Testing only for a miniforge3
directory is not enough — an operator can have a partial install (e.g. otagent but not
evalchemy-marin) and would then fail with EnvironmentNameNotFound: evalchemy-marin
instead of falling back to the shared root. The check is now for
<root>/envs/evalchemy-marin.

--output directories must pre-exist. SLURM opens the log file before the job script
runs and does not create intermediate directories, so a new operator gets an instant
"Unable to open file" that reads like a launch bug rather than a missing mkdir. The script
cannot self-heal this — its own mkdir -p runs far too late — so the one-time setup is
documented inline next to each path.

Reviewer note

With HF_HUB_OFFLINE=1 and HF_HOME now per-user, a new operator's cache starts empty,
so models will not resolve until they pre-cache or symlink them in. That is inherent to
making the cache writable (one shared HF cache across operators is not safe), and the
symlink workaround is documented in the diff — but it is a genuine behaviour change for a
new user.

Testing

  • bash -n clean on all four scripts.
  • Path resolution simulated for three cases: original operator (all-own, byte-identical),
    new operator (shared fallback for runtime, per-user for writable), and partial install
    (correctly prefers shared over an incomplete own root).
  • Comments-only in the final commit.

🤖 Generated with Claude Code

mrinal-bespoke and others added 5 commits August 4, 2026 11:58
…ed runtime falls back)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… prior commit)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…st the miniforge3 root

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The --output paths are now $USER-scoped via SLURM's %u. That is correct, but
SLURM opens the log file BEFORE the job script runs and does not create
intermediate directories, so a new operator whose
/leonardo_work/AIFAC_5C0_290/$USER/experiments/delphi-eval/ does not exist yet
gets an instant job failure ('Unable to open file') that reads like a launch bug
rather than a missing mkdir.

The job script cannot self-heal this -- its own mkdir -p runs far too late. So
document the one-time setup inline, next to the path it applies to, including
the qwen3-baseline subdirectory that only that sbatch needs.

No behaviour change: comments only. Byte-identical for the original operator,
whose directories already exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l checkout

Review catch (P2). _OTA_REPO defaulted to $WORK/$USER/code/OpenThoughts-Agent,
which only holds for operators who cloned at exactly that path. Anyone else fell
through to the BASH_SOURCE branch — which under SLURM is the
/var/spool/slurmd/jobNNN/ copy — so it re-created the very ExitCode-127 'No such
file or directory' that the comment three lines above says this block exists to
prevent.

The blast radius was wider than the exec target. _OTA_REPO also feeds
CHAT_TEMPLATE_OVERRIDE (delphi_eval) and THINK_TEMPLATE
(delphi_rl_think_eval), so a noncanonical checkout silently pointed lm-eval at a
missing chat template instead of reporting that the repo was not found.

Resolve by PROBING for a marker file (eval/evalchemy/evalchemy_eval.sbatch),
taking the first candidate that actually contains it:

  1. OTAGENT_REPO_DIR              explicit override
  2. $WORK/$USER/code/...          canonical per-user checkout
  3. SLURM_SUBMIT_DIR + parents    where the operator actually ran sbatch
  4. shared bfeuer00 checkout      same borrow pattern as conda/evalchemy-marin
  5. script dir + parents          interactive / non-SLURM only

Walking up from SLURM_SUBMIT_DIR matters because operators typically submit from
a subdirectory of the repo, not its root.

If nothing matches, fail LOUDLY listing every candidate tried, instead of
exec'ing a nonexistent path and leaving a bare 127.

Unchanged for the original operator: candidate 2 hits first when USER=bfeuer00.

Verified: canonical operator, noncanonical checkout via SLURM_SUBMIT_DIR,
noncanonical with only the spool dir (falls back to shared), explicit override,
and the nothing-found path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@Nativeatom Nativeatom left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good now, with a few adjustment options that potentially increase the robustness.

Force-set → env-overridable: a real behavior change masked by the "no-op" claim (medium)

In the base, HF_HOME, HF_HUB_CACHE, and CACHE_ROOT were force-set (export HF_HOME=…/data/hub, etc.), overriding any ambient value. OUT_ROOT was also force-set in delphi_rl_think_eval.sbatch and qwen3_eval.sbatch (OUT in qwen3). In the head these become ${VAR:-default} (evalchemy_eval.sbatch:80-81,96, delphi_rl_think_eval.sbatch:97-98,103,116, qwen3_eval.sbatch:64-65,72,90).

Consequence: if the submitting shell exports any of these (e.g. a HF_HOME pointing at the read-only $HOME, which is a common Hugging-Face habit, or a CACHE_ROOT from another workflow), SLURM propagates it and the script now honours it instead of the work-FS path. For the original operator this can silently redirect the HF/JIT caches away from the pre-populated work-FS copy and fail the eval (or, worse, write into read-only $HOME). This contradicts the letter of the "no-op" claim, which is only guaranteed when none of these variables are set in the submit environment.

This is consistent with the PR's stated design ("explicit env var still wins"), so it is not an accidental bug — but it is a semantic change that the "no-op" wording understates. A one-line mitigation is to keep these force-set (or force-set to _OTA_WORK default unless an explicit opt-in var is given), or to document that a new/other operator must ensure these are unset.

Fallback validation is inconsistent: _pick uses -e, not a marker (low)

The PR's own "Notable fixes" argues that testing only for a directory is insufficient because a partial install can exist. That reasoning was applied to conda (_pick_conda checks /envs/evalchemy-marin), but not to the other two read-only fallbacks:

evalchemy-marin clone — _pick "${_OTA_WORK}/code/evalchemy-marin" … (evalchemy_eval.sbatch:71, delphi_rl_think_eval.sbatch:93, qwen3_eval.sbatch:57) only tests -e on the directory. A half-created/empty own clone would be chosen over the complete shared one, and the job would then fail at python -m eval.eval instead of falling back.
nltk_data — _pick "${_OTA_WORK}/data/nltk_data" … (evalchemy_eval.sbatch:92) has the same weakness: an empty own dir would shadow the populated shared one.
Same bug class the author already fixed for conda, just at lower likelihood. Recommend testing a marker (e.g. pyproject.toml/eval/eval.py for the clone, tokenizers/punkt for nltk) or reusing _pick_conda-style subdir validation.

OUT_ROOT semantics differ between qwen3 and the others (low)

evalchemy_eval.sbatch:110 and delphi_rl_think_eval.sbatch:116 define OUT = OUT_ROOT/RUN_NAME with OUT_ROOT defaulting to …/experiments/delphi-eval. qwen3_eval.sbatch:90 defines OUT = OUT_ROOT/RUN_NAME but with OUT_ROOT defaulting to …/experiments/delphi-eval/qwen3-baseline. A user who sets OUT_ROOT=/custom gets /custom/ from qwen3, dropping the qwen3-baseline grouping that the default (and the --output log path) provides. Not a bug for default usage, but the meaning of OUT_ROOT is not uniform across the four scripts.

Repo-probe ordering can prefer a stale canonical checkout (informational)

_ota_first_repo checks ${_OTA_WORK}/code/OpenThoughts-Agent (candidate #2) before SLURM_SUBMIT_DIR (candidate #3). If an operator has an outdated checkout at the canonical path but is submitting a newer script from a different clone, candidate #2 wins and the job runs against the stale tree. This preserves the original operator's historical behavior (the old code also hardcoded that checkout), so it is not a regression, but it is a subtlety worth knowing for any operator who keeps multiple checkouts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants