fix(slurm): resolve node IPs to non-loopback addresses - #179
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the SLURM-generated SGLang-disaggregated job script to avoid advertising loopback (127.*) addresses in the per-node peer list, which can break multi-node rendezvous/barriers and cause silent hangs during bring-up.
Changes:
- Switch node resolution from
getent hoststogetent ahostsv4, selecting the first non-127.*IPv4. - Add a fallback that derives an address from local interfaces when name resolution yields only loopback output.
Suppressed comments (1)
src/madengine/deployment/slurm.py:964
- The fallback (
hostname -I ...) is applied for any node that fails to resolve viagetent ahostsv4, but the comment says it should only be used for the local node. As written, an unresolvable peer hostname would silently get replaced with the current node’s IP, producing an incorrect peer list and hard-to-debug hangs. Also, because this uses a pipeline,exit 1inside thewhilebody wouldn’t reliably fail the overall command unless you avoid the pipeline or enablepipefail.
Consider restructuring the loop to avoid scontrol | while ... | tr | sed so you can reliably error out on unresolved non-local nodes, and only use the interface-address fallback when $node matches the local host.
SLURM_NODE_IPS=$(scontrol show hostname ${{SLURM_JOB_NODELIST}} | while read node; do
node_ip=$(getent ahostsv4 "$node" | awk '$1 !~ /^127\\./ {{print $1; exit}}')
if [ -z "$node_ip" ]; then
node_ip=$(hostname -I | tr ' ' '\\n' | grep -vE '^127\\.' | head -n1)
fi
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/madengine/deployment/slurm.py:964
- The fallback is applied to every unresolved hostname, not just the local node as the comment states. If a remote node has a transient NSS/DNS failure (or only an IPv6 record), each task inserts its own interface address into that remote slot, producing different/duplicate peer lists and recreating the barrier hang. Identify the local entry (for example via
SLURM_NODEID/SLURMD_NODENAME) before using the local fallback, and fail the task if a remote entry—or the fallback itself—cannot be resolved; make sure that failure is not swallowed by the surrounding pipeline.
if [ -z "$node_ip" ]; then
node_ip=$(hostname -I | tr ' ' '\\n' | grep -vE '^127\\.' | head -n1)
fi
422cd1d to
2b0068b
Compare
2b0068b to
a631246
Compare
a631246 to
cdc0578
Compare
c4c9676 to
b8c3642
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A failed node enumeration can still produce an empty peer list and bypass the new fail-fast guard.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Balanced
…o the container The generated job script built SGLANG_NODE_IPS with `getent hosts "$node"`. On Ubuntu, /etc/hosts maps the machine's own hostname to 127.0.1.1 and `getent hosts` returns that entry first, so each node resolved *itself* to loopback while resolving its peers correctly. Every node then published a peer list with a loopback address in its own slot, and anything waiting on all peers hung forever: nobody listens on the address its peers were told to connect to. We hit this with the sglang-disagg socket barrier — two nodes sat in wait_for_all_ports() and never launched their servers, which collapsed the 32-rank rendezvous (`TCPStore ... shut down too early`, `client socket has timed out ... 5757`). The run then hangs at bring-up with no error until the SLURM time limit, and looks intermittent because it only bites when an allocated node carries the /etc/hosts self-mapping. Changes: - Resolve each node with `getent ahostsv4`, ignoring 127.* entries. - Only the local node may fall back to its own address; doing that for a peer would publish this node's IP in the peer's slot, which is worse than having no entry. A node that still has no routable address aborts the job and names the offending hosts. - Identify the local node by SLURMD_NODENAME, falling back to the hostnames. The list expands to NodeName values, which differ from the machine's hostname wherever the config sets NodeHostname; matching on hostname alone would treat the local loopback-only entry as an unresolvable peer and abort a job that has a valid address to advertise. - Take the local address from the configured cluster interface (NCCL_SOCKET_IFNAME, first entry of a comma list), else from the source address the kernel would use for outbound traffic. Not `hostname -I`, which lists every interface in unspecified order and is not IPv4-only, so it can hand back a docker bridge or management address. - Forward the generated SGLANG_* variables into the container. sglang-disagg takes the normal Docker path and ContainerRunner only copies an allowlist, which contained none of them — so the corrected peer list stopped at the job script and run.sh fell back to xP=1/yD=1 with IPADDRS=localhost. The allowlist moved to the module-level SLURM_PASSTHROUGH_ENV_VARS with a small _merge_slurm_env_from_shell() helper so it can be tested; no behaviour change. Tests execute the rendered snippet against stubbed ip/getent/hostname/scontrol on a node holding a docker bridge (172.17.0.1), a management address (192.168.1.5) and the cluster interface (10.0.0.2): a resolvable peer keeps its own address, the loopback-mapped local node advertises the cluster interface and is still recognised when its NodeName differs from its hostname, a loopback-only *peer* does not inherit our address, and an unresolvable peer makes the script exit non-zero naming that host. The container handoff is covered separately, both for the allowlist contents and for the copy into docker_env_vars. tests/unit: 593 passed. The two failures in test_database_mongodb.py and test_validators.py reproduce unchanged on upstream/develop (the sandbox raises PermissionError where those tests expect FileNotFoundError). Co-authored-by: Cursor <cursoragent@cursor.com>
b8c3642 to
4de6d9b
Compare
Problem
The generated SLURM job script builds the peer list with:
On Ubuntu,
/etc/hostsmaps the machine's own hostname to127.0.1.1, andgetent hostsreturns that entry first. Since the loop runs on each node, everynode resolves itself to
127.0.1.1while resolving its peers correctly — soeach node publishes a peer list with a loopback address in its own slot.
Anything that waits on all peers then hangs forever: no node listens on the
loopback address its peers were told to connect to. We hit this with the
sglang-disagg socket barrier, where two nodes sat in
wait_for_all_ports()andnever launched their servers, which in turn collapsed the 32-rank rendezvous
(
TCPStore ... shut down too early,client socket has timed out ... 5757).The symptom is a multi-node run that hangs at bring-up with no error until the
SLURM time limit. It is load-dependent: the larger the allocation, the likelier
at least one node carries the
/etc/hostsself-mapping, which makes it lookintermittent.
Fix
Resolve each node with
getent ahostsv4, skipping127.*, and fall back to theprimary non-loopback interface address for the local node.
Verification
127.0.1.1in the local slot;after the fix every node resolves to a routable address and all nodes reach the
barrier.
tests/unit/test_slurm_job_template.py— 21/21 pass.bash -n.Made with Cursor