Skip to content

core: cluster-level environment overrides + trtllm_serve IPv6 bind support - #348

Open
v-shobhit wants to merge 6 commits into
NVIDIA:mainfrom
v-shobhit:user/v-shobhit/vrnvl72-onboarding
Open

core: cluster-level environment overrides + trtllm_serve IPv6 bind support#348
v-shobhit wants to merge 6 commits into
NVIDIA:mainfrom
v-shobhit:user/v-shobhit/vrnvl72-onboarding

Conversation

@v-shobhit

@v-shobhit v-shobhit commented Aug 26, 2026

Copy link
Copy Markdown

Summary

Three related additions surfaced while onboarding a new IPv6-only VRNVL72 rack (nvidia-semianalysis-private runner vrnvl72-nv_0/1/2/6/7/8) to srtctl. All are cluster-scoped (srtslurm.yaml), off by default, and no-ops for existing clusters.

  • ClusterConfig.environment / ClusterConfig.unset_environment — cluster-level env var overrides/removals, applied after the recipe's own backend.{prefill,decode,aggregated}_environment but before (environment) or after (unset_environment) the recipe's own top-level environment: block. Needed when a recipe's baked-in env is correct for one cluster sharing that recipe but wrong for another (e.g. a NIC name or sitecustomize hook that only exists on one rack).
  • Frontend environment parityworker_stage.py already applied ClusterConfig.environment/unset_environment to every backend worker; frontends/{trtllm_serve,sglang,dynamo}.py never did (dynamo applied environment but not unset_environment; trtllm_serve and sglang applied neither). This meant a cluster env fix that worked for backend workers silently didn't reach the frontend/orchestrator process. Confirmed live: the trtllm_serve orchestrator crashed with OSError: [Errno 30] Read-only file system (flashinfer's JIT cache trying to create a directory under an unset HOME) even after the identical cluster override fixed every backend worker.
  • ClusterConfig.trtllm_serve_hostnamefrontend.type: trtllm_serve's disaggregated orchestrator hardcodes ser.yaml hostname: 0.0.0.0 (IPv4-any). On an IPv6-only cluster this is unreachable from any other node in the job: the orchestrator itself answers GET /health with 200 on localhost, but every other node's request to <node>:8000/health fails to connect, hanging srtctl's own wait_for_model() health poll indefinitely even though the full stack is healthy. Defaults to the existing 0.0.0.0 when unset.
  • ClusterConfig.network_interface now accepts a comma-separated, preference-ordered interface list (e.g. "eth13,eth7,eth4"), tried in order per node in get_node_ip.sh's Method 1. Surfaced by a live NIC survey on the same rack: 15 of 18 nodes name their 400GbE NIC eth13, but 3 don't (eth7/eth17 or eth4/eth10) — a single hardcoded name silently no-ops on those nodes (ip addr show <name> returns nothing) and falls through to ip route's default-source-IP guess, which is ambiguous when both of a node's NICs sit on the same subnet/metric. A preference list lets one cluster-wide setting resolve correctly everywhere, and (as a side effect) lets an operator steer a whole cluster away from a known-bad NIC by simply not listing it first.

Test plan

  • Full onboarding validated end-to-end on the new rack: server bringup, health checks, and a complete 1-hour AgentX benchmark profiling phase all pass with zero errors, using unset_environment + environment + trtllm_serve_hostname: "::" set in that cluster's srtslurm.yaml.
  • tests/test_ip_utils.py::test_get_node_ip_falls_through_comma_separated_interface_list added for the new preference-list resolution; full suite run (uv run pytest tests/) shows no regressions from this branch (the 4 failures present are pre-existing/environment-local, reproduced identically with this branch's commits stashed out).
  • Unit tests for ClusterConfig.environment/unset_environment and the frontend env-application parity (not yet added — flagging for review since CLAUDE.md calls out that new config affecting srun should come with test_dry_run.py coverage)

🤖 Generated with Claude Code

v-shobhit and others added 4 commits August 26, 2026 00:30
A recipe shared across multiple clusters (e.g. two racks both running
the same disagg config) can only carry one value for a NIC-dependent
env var like NCCL_SOCKET_IFNAME/UCX_NET_DEVICES in its
backend.{prefill,decode}_environment blocks -- correct for one
cluster, silently wrong on another with different interface names.

Confirmed via a live crash on a second VRNVL72-type rack: a recipe
hardcoding UCX_NET_DEVICES=eth0 (correct on the original rack) caused
NIXL's UCX transfer-agent setup to abort() on worker startup, since
eth0 isn't a valid UCX device there (only InfiniBand devices are).

Add ClusterConfig.environment (srtslurm.yaml, cluster-local, not
recipe-shared) and merge it into RuntimeContext.environment between
backend env and the recipe's own top-level environment: block, so
cluster-level overrides win over the recipe's hardcoded backend env
but the recipe's explicit environment: block still wins if set --
same precedence shape as the existing default_mounts mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ClusterConfig.environment (previous commit) can override a value but
not remove it. Some cluster-vs-recipe mismatches have no correct
replacement value to override with -- e.g. a recipe's
UCX_NET_DEVICES/NCCL_SOCKET_IFNAME pinned to a device name from
another cluster's topology (a plain NIC name on one cluster, an
InfiniBand HCA name like cuda0-mlx5_0:1 on another -- not even the
same naming scheme). The correct fix there is to omit the variable
entirely and let UCX/NCCL auto-select, which an override can't express.

Add ClusterConfig.unset_environment (list of keys) and pop them from
env_to_set last, after every other environment source, at both
worker-env-assembly call sites.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…esses

worker_stage.py already applies ClusterConfig.environment/unset_environment to
every backend worker, but frontend launch (dynamo.py, sglang.py,
trtllm_serve.py) only ever saw config.frontend.env -- silently missing
cluster overrides like a writable HOME/TMPDIR. Confirmed live: the
trtllm_serve orchestrator crashed with
OSError: [Errno 30] Read-only file system: '.../.cache' (flashinfer's JIT
cache trying to create a directory under an unset HOME) despite the same
cluster override successfully fixing every backend worker. dynamo.py already
applied runtime.environment but not unset_environment; sglang.py and
trtllm_serve.py applied neither. All three now match worker_stage.py's
pattern.
frontend.type: trtllm_serve's disaggregated orchestrator hardcodes
ser.yaml hostname: 0.0.0.0. On an IPv6-only cluster this is unreachable from
any other node in the job -- confirmed live (job 1120): the orchestrator
answers GET /health with 200 on localhost, but curl from the orchestrator's
own node to <node>:8000/health returns exit 7 (connection failed) from every
other node, hanging srtctl's own wait_for_model() health poll indefinitely
even though the full stack is healthy. Added ClusterConfig.trtllm_serve_hostname
(defaults to the existing 0.0.0.0 when unset, so this is a no-op for every
other cluster) so an IPv6-only cluster can set it to '::'.
@codecov-commenter

codecov-commenter commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.87500% with 9 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@a19d945). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/srtctl/frontends/trtllm_serve.py 20.00% 4 Missing ⚠️
src/srtctl/cli/mixins/worker_stage.py 60.00% 2 Missing ⚠️
src/srtctl/core/runtime.py 83.33% 1 Missing ⚠️
src/srtctl/frontends/dynamo.py 66.66% 1 Missing ⚠️
src/srtctl/frontends/sglang.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #348   +/-   ##
=======================================
  Coverage        ?   72.14%           
=======================================
  Files           ?       96           
  Lines           ?    13117           
  Branches        ?        0           
=======================================
  Hits            ?     9463           
  Misses          ?     3654           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ClusterConfig.network_interface now accepts a comma-separated,
preference-ordered list of interface names (e.g. "eth13,eth7,eth4"),
tried in order per node in get_node_ip.sh's Method 1. This lets one
cluster-wide setting cover a fleet where the same physical NIC has
different names on different nodes, instead of silently falling
through to ip-route-based auto-selection (ambiguous when both NICs
share a subnet/metric) on any node where the single hardcoded name
doesn't exist.
… sites

RuntimeContext.head_node_ip/infra_node_ip and four frontend/backend
call sites (trtllm_serve health-check URL, sglang frontend leader IPs,
nginx frontend_hosts, sglang backend leader IP) called get_hostname_ip()
without network_interface at all, silently bypassing the cluster's
setting (including the new preference-list behavior) and always falling
through to ip-route-based auto-selection.

Caught live: on a rack with a preference-ordered network_interface list,
one prefill worker on a node lacking the first-preference interface
still resolved via the ambiguous route-based fallback instead of the
node's correct second-preference NIC, because its health-check URL was
built through frontends/trtllm_serve.py's unguarded call.

The vLLM Dynamo-resolution call site is intentionally left unguarded
per the existing comment there (scoped to avoid behavior change outside
the direct-vLLM frontend); VLLM_NIXL_SIDE_CHANNEL_HOST's enclosing
function had no runtime in scope, so it reads network_interface via
get_srtslurm_setting() directly instead of threading a new parameter
through its callers.

Updates two tests whose get_hostname_ip mocks assumed the old
single-argument signature.
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