feat: add Slurm command client and renderer - #892
Conversation
Greptile SummaryThe PR adds a structured Slurm command boundary and deterministic batch-script renderer.
|
| Filename | Overview |
|---|---|
| packages/data-designer-slurm/src/data_designer/slurm/launcher/client.py | Adds typed Slurm command construction, output correlation, and normalized command failures without an eligible follow-up defect. |
| packages/data-designer-slurm/src/data_designer/slurm/launcher/parsing.py | Adds strict parsers for scheduler identities, states, exit codes, submission receipts, and GPU resources. |
| packages/data-designer-slurm/src/data_designer/slurm/launcher/renderer.py | Adds deterministic batch rendering with validated directives, quoted values, checksums, and attempt isolation. |
| packages/data-designer-slurm/src/data_designer/slurm/launcher/runner.py | Adds bounded shell-free process execution and now correctly preserves either the caller PATH or the platform fallback path. |
| packages/data-designer-slurm/tests/launcher/test_runner.py | Covers minimal environment handling, including absent and empty ambient PATH behavior. |
Sequence Diagram
sequenceDiagram
participant Caller
participant Client as SlurmCommandClient
participant Runner as SubprocessRunner
participant Slurm as Slurm CLI
participant Parser
Caller->>Client: submit/query/cancel
Client->>Runner: structured argv
Runner->>Slurm: bounded subprocess with minimal environment
Slurm-->>Runner: stdout/stderr/return code
Runner-->>Client: completed process
Client->>Parser: parse machine-readable output
Parser-->>Caller: typed scheduler result
Reviews (9): Last reviewed commit: "simplify Slurm launcher structure" | Re-trigger Greptile
| raise ValueError("environment names must be non-empty and must not contain '=' or NUL") | ||
| if type(value) is not str or "\0" in value: | ||
| raise ValueError("environment values must not contain NUL") | ||
| self._environment = MappingProxyType({**explicit_environment, "LC_ALL": "C"}) |
There was a problem hiding this comment.
Default environment loses Slurm PATH
If a cluster installs Slurm outside the platform fallback path, the default runner replaces the configured environment with only LC_ALL=C while using bare executable names, causing every default client operation to fail with SlurmCommandError even though the tools are available through the caller's PATH.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-slurm/src/data_designer/slurm/launcher/runner.py
Line: 43
Comment:
**Default environment loses Slurm PATH**
If a cluster installs Slurm outside the platform fallback path, the default runner replaces the configured environment with only `LC_ALL=C` while using bare executable names, causing every default client operation to fail with `SlurmCommandError` even though the tools are available through the caller's `PATH`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in bb5bc8e. The default runner now forwards only the caller's PATH plus LC_ALL=C; an explicitly supplied environment remains an exact replacement. This preserves cluster command discovery without unrestricted ambient-environment forwarding. The environment regression coverage and the full Slurm suite pass (490 tests after the follow-up fix).
| if type(timeout_seconds) not in {int, float} or not math.isfinite(timeout_seconds) or timeout_seconds <= 0: | ||
| raise ValueError("timeout_seconds must be a finite positive number") | ||
| explicit_environment = ( | ||
| dict(environment) if environment is not None else {"PATH": os.environ.get("PATH", os.defpath)} |
There was a problem hiding this comment.
Empty PATH bypasses fallback resolution
If the parent environment contains PATH="", os.environ.get preserves the empty value instead of using os.defpath. The default client's bare Slurm executable names then cannot resolve, causing every operation to fail with SlurmCommandError even when the tools are installed in a platform fallback directory.
| dict(environment) if environment is not None else {"PATH": os.environ.get("PATH", os.defpath)} | |
| dict(environment) if environment is not None else {"PATH": os.environ.get("PATH") or os.defpath} |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-slurm/src/data_designer/slurm/launcher/runner.py
Line: 39
Comment:
**Empty PATH bypasses fallback resolution**
If the parent environment contains `PATH=""`, `os.environ.get` preserves the empty value instead of using `os.defpath`. The default client's bare Slurm executable names then cannot resolve, causing every operation to fail with `SlurmCommandError` even when the tools are installed in a platform fallback directory.
```suggestion
dict(environment) if environment is not None else {"PATH": os.environ.get("PATH") or os.defpath}
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in ca308e2 by falling back to os.defpath when the ambient PATH is absent or empty. Added a regression test for an empty PATH; the full Slurm suite passes (490 tests), and Slurm lint and formatting checks pass.
| "REVOKED": SchedulerState.FAILED, | ||
| "RUNNING": SchedulerState.RUNNING, | ||
| "SIGNALING": SchedulerState.RUNNING, | ||
| "SPECIAL_EXIT": SchedulerState.FAILED, |
There was a problem hiding this comment.
Mapping SPECIAL_EXIT to FAILED makes a live held job appear terminal. Since this state parser is shared by squeue observations, reconciliation could start a replacement attempt while the original job is still held and releasable. This should remain nonterminal, or queue and accounting states should use separate normalization paths. A focused held-state test would pin the intended behavior. Slurm state definitions
There was a problem hiding this comment.
Fixed in 7f84dcf. SPECIAL_EXIT now normalizes to the nonterminal PENDING state, consistent with a held and releasable job. Focused state tests now pin REQUEUE_HOLD, RESV_DEL_HOLD, and SPECIAL_EXIT as nonterminal.
| "--array", | ||
| "--allocations", | ||
| "--parsable2", | ||
| "--format=JobIDRaw,State,ExitCode", |
There was a problem hiding this comment.
JobIDRaw does not provide the logical <array_job_id>_<task_id> identity expected here. Real array elements are returned as numeric allocation IDs, which parse_accounting silently skips, turning terminal accounting evidence into an empty result. Switching this to JobID and updating the fake and goldens would align the command with the parser. It would also be safer for unexpected ID shapes to raise instead of being discarded. Slurm field definitions
There was a problem hiding this comment.
Fixed in 7f84dcf. The accounting query now requests JobID, and the fake Slurm command contract was updated to match. parse_accounting still ignores the numeric array-parent row, but unexpected step-shaped or otherwise malformed IDs now raise instead of being silently discarded. The command, fake, and parser regression tests cover the new behavior.
| _EXIT_CODE_PATTERN = re.compile(r"^(?P<status>[0-9]+):(?P<signal>[0-9]+)$") | ||
| _GRES_GPU_PATTERN = re.compile(r"^gpu:(?:(?:[^:,()]+):)*(?P<count>[1-9][0-9]*)(?:\([^\r\n]*\))?$") | ||
|
|
||
| _STATE_MAP = { |
There was a problem hiding this comment.
One smaller state-coverage gap: RESV_DEL_HOLD currently falls through to UNKNOWN, although Slurm defines it as held. The fallback is safe, so this is not merge-blocking, but an explicit held or pending mapping would preserve the scheduler state more accurately.
There was a problem hiding this comment.
Fixed in 7f84dcf. RESV_DEL_HOLD now maps explicitly to the nonterminal PENDING state, with focused coverage alongside the other held-state spellings.
| if plan.array_tasks.count > 1: | ||
| array = f"0-{plan.array_tasks.count - 1}%{plan.array_tasks.max_concurrent}" | ||
|
|
||
| values: list[tuple[str, str | None]] = [ |
There was a problem hiding this comment.
Could the parent allocation reserve the CPUs required by client.cpus? That value defaults to 32, but the rendered directives do not request CPUs. On sites without whole-node allocation or a sufficient DefCpuPerGPU, the later client step cannot request resources the allocation does not hold. Another valid approach would be to encode and validate that site policy in SlurmProfile; either way, both GPU request modes need coverage.
There was a problem hiding this comment.
Fixed in 7f84dcf. The parent batch allocation now renders #SBATCH --cpus-per-task=<client.cpus> from the resolved client configuration. The renderer tests cover both gres and visible GPU request modes, and the pinned goldens now record the default 32-CPU claim.
andreatnvidia
left a comment
There was a problem hiding this comment.
The follow-ups resolve the accounting identity, CPU allocation, and held-state issues from the earlier review. The ordinary-job and array-task observation paths now preserve the expected selector and accounting-lag behavior. I do not see any remaining merge blockers; the unreachable unthrottled-array branch is optional cleanup.
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Preserve the caller's PATH as the only ambient lookup input for default Slurm commands while continuing to isolate all other environment variables. Normalize oversized numeric scheduler fields into the launcher parse-error boundary. Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Use the platform default search path when the ambient PATH is absent or empty so bare Slurm executables remain resolvable. Part of #868 Signed-off-by: Nabin Mulepati <nmulepati@nvidia.com>
Support both ordinary jobs and array-task observations while preserving accounting-lag semantics. Bound scheduler numeric fields and diagnostics, and honor unthrottled plan arrays when concurrency is omitted.
- Internalize implementation APIs and clarify transient types - Harden parsing, rendering, and process error normalization - Extend fakes and executable checksum coverage Part of #868
94ad592 to
d5db22e
Compare
📋 Summary
Adds the first independently reviewable #868 slice: a structured Slurm command boundary and deterministic thin batch renderer. This gives M0 a plan-to-script proof and supplies the scheduler primitives needed by #867 without pulling allocation runtime or persistence policy into this PR.
🔗 Related Issue
Part of #868
🔄 Changes
sbatch,squeue,sacct,scancel, and boundedsinfooperations with exact argv construction, minimal environments, timeouts, normalized errors, and returned-selector correlation.🔍 Attention Areas
client.py— managed-selector correlation and the--export=NILsubmission boundary.renderer.py— directive/resource semantics and checksum-before-source ordering.🧪 Testing
make testpasses — 4,564 passed, 1 skippedmake check-allpassesmake test-slurm-wheel-installpasses✅ Checklist