slurm: batch the queries and parallelize the writes in the GPU power/clock helpers - #1393
Conversation
…clock helpers set_gpu_power_levels.sh and set_gpu_clocks.sh called nvidia-smi once per GPU to read the target value and once more to apply it, all serially. Both "nvidia-smi -pl" and "nvidia-smi -ac" take roughly a second per GPU, so on an 8-GPU node the two helpers together add about 8 s to the prolog of every job that 50-exclusive-gpu runs for. srun reports this as: srun: Prolog hung on node <node> Read the values for all GPUs in a single --query-gpu call, then apply them in parallel and collect each child's exit status so a failure on any GPU still fails the script. Behaviour is otherwise unchanged: the same values are written to the same GPUs. The "default" branch of set_gpu_clocks.sh already operated on all GPUs at once and is untouched. Observed on DGX OS 7.5.0 (8x B300), Slurm 26.05.1: prolog took 6-8 s per job while 50-exclusive-gpu was running. Signed-off-by: Jea-Eok-Kim <je.kim@xiilab.com>
dholt
left a comment
There was a problem hiding this comment.
The batched queries at set_gpu_power_levels.sh:21 and set_gpu_clocks.sh:13-14 run in process substitutions, whose failures are invisible to readarray and set -e. If a query fails with empty output, the loop is empty and the helper exits 0; partial output can update only some GPUs and also return success. Capture each query through a construct whose status can be checked, validate that all required per-GPU rows are present and aligned, and only then launch writes. Please cover empty, partial, and nonzero query results as required by the changed-path evidence.
Automated triage review (agent-generated on the maintainer's behalf; a human maintainer decides merges).
`readarray -t limits < <(nvidia-smi ...)` hides the query's exit status from
both readarray and `set -e`. The helpers therefore reported success in every
failure mode: an empty result made the write loop run zero times, a truncated
result configured only some of the GPUs, and a nonzero exit was not seen at all.
The query result now goes through a file so its status can be checked, the index
is selected alongside the values so a write targets the GPU nvidia-smi reported
instead of an array subscript, and every row is parsed and range-checked before
the first write is launched. The row count is compared against `nvidia-smi -L`,
so a partial result is rejected rather than silently applied.
set_gpu_clocks.sh additionally selected clocks.max.mem and clocks.max.sm in two
separate queries. If the two returned different row counts, `${maxMEM[$i]}` was
empty for the trailing GPUs and produced an `-ac ,1980` argument. Both values
now come from the same query, so they cannot drift out of alignment.
Verified on a DGX B300 (Ubuntu 24.04, bash 5.2) with an nvidia-smi stub that
honours --query-gpu and records writes instead of performing them. Identical
results for both helpers:
case before after
---------- ---------------------------- ---------------------
ok rc=0 8 writes rc=0 8 writes
empty rc=0 0 writes rc=1 0 writes
partial rc=0 3 writes (of 8 GPUs) rc=1 0 writes
fail rc=0 0 writes rc=1 0 writes
nonnumeric rc=0 8 writes ("N/A" passed) rc=1 0 writes
One limitation of the stub is worth stating: it records writes rather than
performing them, so the `nonnumeric` row shows 8 writes for the old code. On
real hardware `nvidia-smi -pl N/A` fails and `wait` would surface rc=1 — but
only after eight bad invocations. The new code rejects the row before the first
one.
The `ok` case is unchanged, so the parallel-write speedup this branch adds is
preserved.
|
Thanks — the process-substitution point is correct, and the failure modes were worse than "invisible": all four of them returned success. What changed (commit
Coverage of the cases you asked for, verified on a DGX B300 (Ubuntu 24.04, bash 5.2) with an
One limitation of the stub is worth stating plainly: it records writes rather than performing them, so the The |
dholt
left a comment
There was a problem hiding this comment.
The checked primary queries and explicit child waits address the earlier failure-handling concerns. Two issues remain in the GPU helper templates:
- Their Bash array-length expressions are interpreted as Jinja comment starts when Ansible renders the files, producing a missing-comment-end error. Protect the literal Bash expressions and add a test that renders the actual templates.
- The secondary
nvidia-smi -L | grep -cpipeline still loses the producer's exit status. Partial output followed by a nonzero exit can pass validation and reach GPU writes. Check that command's status before parsing/counting, with a regression for partial output plus failure.
These are specific to the changed helpers; the separate epilog ownership work is tracked in #1407.
…'s status Two corrections from review. Both helpers counted validated rows with bash's array-length expansion. These files are installed with the template module, and the brace-hash sequence that expansion needs opens a Jinja comment, so `ansible.builtin.template` aborted with "Missing end of comment tag" before either script could run. Neither file uses Jinja at all, so the count is now kept in a variable incremented as rows are read, rather than adding escaping machinery to a shell script. `scripts/deepops/check-template-syntax.py` parses every role template so this class of breakage is caught in CI. It only parses, never renders, so no variables are needed; templates that use Ansible-provided filters raise TemplateAssertionError and are reported as skipped rather than failed. The GPU count came from `$(nvidia-smi -L | grep -c '^GPU ')`, whose status is grep's. A listing that emitted every line and then exited nonzero produced a count that matched the query rows, passed validation and reached the writes. The listing is now captured on its own and its status checked before the count is taken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both remaining points are addressed in 1. Jinja comment collisionReproduced first, parsing the templates exactly as they stood on this branch: Neither file uses Jinja at all ( rows=0
...
indexes+=("$index")
limits+=("$limit")
rows=$((rows + 1))
done < "$tmp"
if [ "$rows" -ne "$expected" ]; thenAfter the change, both parse: 2. Test that renders the actual templates
Current tree: Unknown filters raise I did not wire it into a workflow in this PR — say the word if you would rather have it as a step in 3.
|
| exit | GPU writes | |
|---|---|---|
before, set_gpu_power_levels.sh |
0 | 8 |
after, set_gpu_power_levels.sh |
1 | 0 |
before, set_gpu_clocks.sh |
0 | 8 |
after, set_gpu_clocks.sh |
1 | 0 |
Case B — everything healthy (8 GPUs listed, exit 0):
| exit | GPU writes | |
|---|---|---|
after, set_gpu_power_levels.sh |
0 | 8 |
after, set_gpu_clocks.sh |
0 | 8 |
My first attempt at case A had the listing emit only 2 of 8 lines, which the old code already caught on the row-count mismatch — that shape was not the hole. The table above is the corrected one.
dholt
left a comment
There was a problem hiding this comment.
The revision fixes the two previously requested helper issues. CI now flags the syntax checker's implicit autoescape=False on the Jinja environment.
This helper only compiles templates and does not render HTML, so I do not see an HTML-output sink here. Making autoescape=True explicit is safe for this syntax-only use and does not change how Ansible renders deployment templates. Please apply the small suggestion and rerun CI to clear the CodeQL check.
| templates = os.path.join(roles_dir, role, "templates") | ||
| if not os.path.isdir(templates): | ||
| continue | ||
| env = Environment(loader=FileSystemLoader(templates)) |
There was a problem hiding this comment.
| env = Environment(loader=FileSystemLoader(templates)) | |
| env = Environment(loader=FileSystemLoader(templates), autoescape=True) |
There was a problem hiding this comment.
Applied in 76b30885. Verified with jinja2 3.1.6 that the checker's output is byte-identical before and after: 82 templates parsed, 5 skipped, 0 failed in both runs.
CodeQL flags the implicit autoescape=False on the Jinja environment. The checker only compiles templates and never renders them, so autoescape has no effect on its behaviour, but leaving it implicit keeps the alert open. Verified with jinja2 3.1.6 that the output is byte-identical before and after: 82 templates parsed, 5 skipped, 0 failed in both runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Applied in I checked that it is inert here rather than taking it on the reasoning alone. With jinja2 3.1.6, running the checker before and after the change gives byte-identical output — Same five skips in both runs ( One process note against myself: my first attempt at this comparison used a |
dholt
left a comment
There was a problem hiding this comment.
The current revision resolves the requested query-status, listing-status, template-rendering and syntax-checker findings. Independent review found no remaining functional blocker.
Validation: actual Ansible rendering of both helpers; 78 inert helper invocations including historical negative controls, sparse/reordered indices, malformed/failed queries and listings, all child-failure positions, and complete child waits. A separate 33-case rendered-helper run and focused role syntax check also passed. The syntax checker reports 82 parsed, 5 unknown-filter skips, 0 failures. All 15 current-head CI checks are green, and the earlier CodeQL thread is answered.
This validates the error handling and rendered scripts, not real-GPU timing or deployment compatibility. Retaining these isolated regressions in CI is a nonblocking follow-up. The unrelated epilog ownership work remains separate.
Problem
set_gpu_power_levels.shandset_gpu_clocks.shcallnvidia-smionce per GPU to read the target value and once more to apply it, all serially. Bothnvidia-smi -plandnvidia-smi -actake roughly a second per GPU, so on an 8-GPU node the two helpers together add about 8 s to the prolog of every job for which50-exclusive-gpuruns.srunsurfaces it as:Observed on DGX OS 7.5.0 (8× B300), Slurm 26.05.1.
Fix
--query-gpucall (the per-GPU-iloop was only needed because the value was read one at a time).The same values are written to the same GPUs; only the number of
nvidia-smiinvocations and their concurrency change. Thedefaultbranch ofset_gpu_clocks.shalready operated on all GPUs at once and is untouched.Verification
Not yet timed with the patched scripts on hardware — the system where this was found has
50-exclusive-gpuremoved fromprolog.d(an 8-GPU node shared between jobs should not have every job reset limits and clocks on all GPUs). The serial cost is reproducible there: prolog took 6–8 s per job while the script was in place. Marked as draft for that reason; happy to run a timed before/after if that would help.Related
The reason every job ran
50-exclusive-gpuin the first place is a separate defect in the exclusive-job detection, addressed in #1391.