harness: run outside HTCondor, and stop the eval reaping other tenants' GPU processes - #71
Open
tangxiangru wants to merge 1 commit into
Open
harness: run outside HTCondor, and stop the eval reaping other tenants' GPU processes#71tangxiangru wants to merge 1 commit into
tangxiangru wants to merge 1 commit into
Conversation
…lling other tenants' GPUs
Six fixes found by running the benchmark on a shared Slurm cluster. Every new
knob defaults to the current behaviour, so a site that sets nothing gets exactly
what it gets today.
1. `nvidia-smi --query-compute-apps=pid | xargs -r kill -9` before each
evaluation kills every GPU process on the node, for every user. That is safe
under a whole-node HTCondor claim and destructive anywhere else. Replaced by
`reap_gpu_processes`, scoped with `-i $POST_TRAIN_BENCH_VISIBLE_GPUS` when set
and switchable with `POST_TRAIN_BENCH_EVAL_GPU_REAP=all|own|none`. Default
`all` with no visible-GPU list is the current command.
2. `--env HF_HOME` alone does not redirect the hub cache. huggingface_hub reads
`HF_HUB_CACHE`, else `HUGGINGFACE_HUB_CACHE`, else `$HF_HOME/hub`; HF_HOME is
the lowest-priority of the three and these execs do not pass `--cleanenv`, so
a host that exports `HUGGINGFACE_HUB_CACHE` wins inside the container. The
download then lands on the `--writable-tmpfs` container root and dies as
`OSError: [Errno 28] No space left on device` four frames below anything that
names a cache. All three variables are now set. Same fix in run_baseline.sh.
3. `XDG_CACHE_HOME` is bound through when it exists. vLLM's `VLLM_CACHE_ROOT`
defaults to `$XDG_CACHE_HOME/vllm` and triton's kernel cache lands there too;
unbound, both write to the 64 MiB container root and the second one takes
EngineCore and the vLLM server with it.
4. The evaluation container never bound the results dir, so with an absolute
`POST_TRAIN_BENCH_RESULTS_DIR` the scorer wrote `metrics.json` where nothing
could read it. `--bind "${EVAL_DIR}:${EVAL_DIR}"` added, and a missing
`metrics.json` after the last retry is now fatal instead of a green exit.
This is the one intentional behaviour change.
5. Both `source` lines resolve against `${BASH_SOURCE[0]}` rather than the
working directory, so a launcher may run a node-local copy of src/. bash
holds an open handle on its own inode for the whole run: replacing this file
mid-run killed two 10 h jobs with `error reading input file: Stale file
handle`, after the agent phase and before scoring.
6. `parse_trace.py` dispatches on a substring of the agent name, so an agent
directory named `claude_*` gets the Claude stream-json parser whatever it
actually writes -- a stub output plus one `NOT PARSABLE` line per input line
into error.log. New `--raw-only` takes the path already there for an
unrecognised agent (copy verbatim, still sanitize). run_task.sh passes it when
the agent ships a `payload/` directory.
Also, guarded on files no current agent has, so inert for all of them: an agent
may ship `payload/` (copied to /home/ben/agent, with BENCHMARK_ID,
MODEL_TO_TRAIN and NUM_HOURS in the env) and `env_passthrough.txt` (names only,
no values, for an agent that authenticates against a cloud endpoint rather than
with a provider API key). `POST_TRAIN_BENCH_TMP_ROOT` makes the hardcoded /tmp
scratch overridable for a scheduler that does not reserve 400 G there, and
`POST_TRAIN_BENCH_ISOLATE_GPUS=1` adds `-c --nvccli` so the agent's sandbox gets
a device cgroup rather than an environment variable it can overwrite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Six fixes found while running the benchmark on a shared Slurm cluster instead of an HTCondor whole-node claim. Every new knob defaults to today's behaviour — a site that sets nothing gets exactly what it gets now. No task, prompt, scoring or judge logic is touched.
Two changes are not behind a knob and do change what a current site sees; they are listed under "Unconditional changes" below rather than folded into the sentence above.
Three files:
src/run_task.sh,src/baselines/run_baseline.sh,src/trace_parsing/parse_trace.py.1. The pre-eval GPU reap kills every user's processes
Under a whole-node HTCondor claim nothing else is running, so this is fine. On any shared node it is not: it kills other tenants' training jobs. Replaced with
reap_gpu_processes, scoped to-i $POST_TRAIN_BENCH_VISIBLE_GPUSwhen that is set, and switchable viaPOST_TRAIN_BENCH_EVAL_GPU_REAP=all|own|none. With both unset the command issued is the one above.2.
--env HF_HOMEdoes not actually redirect the hub cachehuggingface_hubresolvesHF_HUB_CACHE, elseHUGGINGFACE_HUB_CACHE, else$HF_HOME/hub—HF_HOMEis the lowest priority of the three. Theseapptainer execs do not pass--cleanenv, so on a host that exportsHUGGINGFACE_HUB_CACHEglobally that value wins inside the container, the overlay atTMP_HF_CACHEis never read, and the host path is not bound in. The hub then materialises the cache on the container root, which--writable-tmpfscaps atsessiondir max size(64 MiB by default), and a multi-GB download dies asfrom inside
file_download.py, four frames below anything that mentions a cache. All three variables are now named. Same fix inrun_baseline.sh.3.
XDG_CACHE_HOMEis bound through when it existsvLLM's
VLLM_CACHE_ROOTdefaults to$XDG_CACHE_HOME/vllm, and triton's kernel cache lands under the same root. Unbound, both write to the 64 MiB container root: the first is survivable (Error saving model info cache), the second istorch._inductor.exc.InductorError: OSError: [Errno 28], which kills EngineCore and takes the vLLM server with it. One bind covers both and keeps compiled kernels across runs; the HF cache deliberately still points at the overlay, so the scorer cannot write into a shared hub.4. The evaluation container never bound the results dir
EVAL_DIRandmetrics.jsonlive underPOST_TRAIN_BENCH_RESULTS_DIR, but onlyREPO_ROOTand the HF cache are bound into the scoring container.example.env's relativeresultshappens to fall insideREPO_ROOT, which is why this has not bitten before; an absolute results dir writesmetrics.jsonwhere nothing can read it. Adds--bind "${EVAL_DIR}:${EVAL_DIR}", and makes a missingmetrics.jsonafter the last retry fatal rather than a green exit.This is the one intentional behaviour change in the PR. Today a run whose every evaluation attempt failed exits 0 with no metrics.
5.
sourceresolves against the script, not the working directorybash reads a script incrementally and seeks back to the byte after the last command it parsed, so a long-running script holds an open handle on its own inode for its whole run. Replacing
src/run_task.shmid-run killed two 10-hour jobs with— each after its agent phase had finished and before it was scored. Both
sourcelines now resolve against${BASH_SOURCE[0]}, so a launcher can run a node-local copy ofsrc/while leaving the working directory on the shared checkout. The working directory still has to be the checkout:REPO_ROOTcomes frompwdand the scoring container binds it by that path.6.
parse_trace.pydispatches on a substring of the agent nameAny agent directory named
claude_*gets the Claude CLI's stream-json parser regardless of what it writes, producing a stub output plus oneNOT PARSABLEline per input line on stderr — intoerror.log, the file meant to hold the harness's own errors. New--raw-onlytakes the path already there for an unrecognised agent (copy verbatim, still sanitize);run_task.shpasses it when the agent ships apayload/directory.Also included, inert for every agent in the repo today
Both guarded on files no current agent has:
agents/<agent>/payload/is copied to/home/ben/agentand the sandbox getsBENCHMARK_ID,MODEL_TO_TRAINandNUM_HOURS— for an agent that is a repository rather than a singlesolve.sh.agents/<agent>/env_passthrough.txtlists variable names only, no values, so an agent that authenticates against a cloud endpoint with ambient host credentials can be run without inventing a provider API key.And two opt-in knobs:
POST_TRAIN_BENCH_TMP_ROOT(default/tmp) — the HTCondor submit file asks forrequest_disk=400Gand gets it on/tmp; a scheduler that makes no such reservation may have far less there.POST_TRAIN_BENCH_ISOLATE_GPUS=1— adds-c --nvccliso the agent sandbox gets a device cgroup.CUDA_VISIBLE_DEVICESis an environment variable, not a fence:--nvbinds all/dev/nvidia*, so the agent's own shell sees every card and is oneexportaway from using them. Measured on an 8×H100 node withNVIDIA_VISIBLE_DEVICES=0:--nvcclialone leaves all eight devices in the sandbox,-c --nvcclileaves one.Unconditional changes
These two are not opt-in and will change behaviour on an existing HTCondor site. Both are
reporting fixes — no task, prompt or scoring logic moves — but they are called out separately
because the "defaults to today's behaviour" line above does not cover them.
SOLVE_EXITnow reports the agent's exit code. The brace group around the agent endedwith
kill $MONITOR_PID, so its status was the kill's, and the group was piped intotimestamp_lines.py, so the pipeline's status was python's. Both are 0 essentially always,so a cell whose
check_cuda.pyrefused to start the agent recordedexit_code: 0 / status: exited normallynext tofinal_model_files: 0.set -o pipefailplus an explicitexit $SOLVE_RCmakes the field mean its label. Nothing downstream branches on it, so theonly visible effect is that some runs which recorded 0 will now record the real code.
run_task.shexits 1 when nometrics.jsonwas written. The script previously ended onan
echo, so six failed evaluation attempts still exited 0 and the scheduler recorded thejob as successful over an empty result. The check is on the artifact rather than on either
retry ladder's return value, because the first ladder failing and the second succeeding is a
normal run. If your tooling treats a nonzero exit as an alert, runs that silently produced no
score will start alerting.
Happy to drop either one if you would rather they landed separately.
Testing
These have carried a full campaign on 8×H100 Slurm nodes — GSM8K post-training runs at the 1 h and 10 h budgets, scored end to end. Not run against HTCondor, which is why every change is default-off or byte-equivalent to the current command; the HTCondor path is what the defaults reproduce.
Happy to split this into six PRs if that is easier to review.