[https://nvbugs/6435121][fix] Eliminate the trtllm-serve port reservation race with --port 0 + --report_addr - #17460
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe changes add atomic bound-address reporting, support kernel-assigned ports, and update integration workflows to discover server and worker addresses at runtime. Worker registration and respawn handling now use ChangesDynamic address publication and worker discovery
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TestWorkflow
participant Server
participant ReportFile
participant Worker
participant ClusterInfo
TestWorkflow->>Server: Start with port=0 and report_addr
Server->>ReportFile: Publish bound host and port
TestWorkflow->>ReportFile: Wait for reported address
TestWorkflow->>Worker: Start with port=0 and resolved configuration
Worker->>ClusterInfo: Register assigned port and worker index
TestWorkflow->>ClusterInfo: Request registered workers
ClusterInfo-->>TestWorkflow: Return worker endpoints
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
tests/integration/defs/disaggregated/disagg_test_utils.py (1)
325-348: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd type annotations to the new URL-discovery helper.
Annotate
get_registered_worker_urls()and nested_urls(). The public return type istuple[list[str], list[str]].As per coding guidelines, “Annotate every function.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/defs/disaggregated/disagg_test_utils.py` around lines 325 - 348, Add type annotations to get_registered_worker_urls, including an int parameter and tuple[list[str], list[str]] return type. Annotate the nested _urls helper with its role_key parameter as str and its return type as list[str], preserving the existing URL discovery behavior.Source: Coding guidelines
tests/integration/defs/common.py (1)
681-762: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd type annotations to the new helper functions.
Annotate
get_ephemeral_port_range(),get_static_port_range(),reserve_port_from_range(), andget_free_port_in_ci(). Use Python 3.10 union syntax for optional results.As per coding guidelines, “Annotate every function.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/defs/common.py` around lines 681 - 762, Add type annotations to get_ephemeral_port_range, get_static_port_range, reserve_port_from_range, and get_free_port_in_ci, including parameter types and return types. Use Python 3.10 union syntax for optional return values, and annotate every function consistently with the existing data structures and behavior.Source: Coding guidelines
tests/integration/defs/stress_test/disagg_cancel/harness.py (1)
1908-1910: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPreformat the new injector log messages.
tensorrt_llm.loggerjoins arguments. It does not apply%sor%dinterpolation. Use one f-string argument for each message.
tests/integration/defs/stress_test/disagg_cancel/harness.py#L1908-L1910: preformat the respawn failure message.tests/integration/defs/stress_test/disagg_cancel/harness.py#L1921-L1926: preformat the missing-registration message.tests/integration/defs/stress_test/disagg_cancel/harness.py#L1931-L1937: preformat the health-wait message.tests/integration/defs/stress_test/disagg_cancel/harness.py#L1989-L1996: preformat invalid-port and polling-failure messages.tests/integration/defs/stress_test/disagg_cancel/harness.py#L1999-L2003: preformat the registration-timeout message.Based on learnings,
tensorrt_llm.loggerjoins arguments rather than performing Python printf-style interpolation; preformat dynamic messages as a single f-string argument.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/defs/stress_test/disagg_cancel/harness.py` around lines 1908 - 1910, Update the injector logging calls in tests/integration/defs/stress_test/disagg_cancel/harness.py at lines 1908-1910, 1921-1926, 1931-1937, 1989-1996, and 1999-2003 to pass each dynamic message as one preformatted f-string argument, covering respawn failures, missing registration, health-wait, invalid-port, polling-failure, and registration-timeout messages.Source: Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/integration/defs/common.py`:
- Around line 687-694: Update get_ephemeral_port_range() to validate the parsed
low and high bounds after reading them, accepting only ranges where 1 <= low <=
high <= 65535. Treat invalid bounds like parsing failures by reporting the
existing diagnostic and returning None, preserving get_static_port_range()’s
documented invalid-range behavior.
In `@tests/integration/defs/disaggregated/disagg_test_utils.py`:
- Around line 340-343: Update the helper containing the port and /cluster_info
checks to add port: int and -> tuple[list[str], list[str]] annotations, raise
ValueError when port is not positive, and raise an explicit exception when the
request status is not 200 instead of using assertions. Preserve the existing
worker extraction behavior for successful responses.
In `@tests/integration/defs/stress_test/disagg_cancel/harness.py`:
- Around line 1908-1913: Update the respawn logic around the tracked.wrapper
assignment so the corresponding entry in self._cluster’s ctx_workers or
gen_workers list is replaced with new_wrapper when respawning begins. Ensure
_teardown_cluster() can terminate the current wrapper for both successful and
failed respawns, preserving the existing tracked-worker bookkeeping.
---
Nitpick comments:
In `@tests/integration/defs/common.py`:
- Around line 681-762: Add type annotations to get_ephemeral_port_range,
get_static_port_range, reserve_port_from_range, and get_free_port_in_ci,
including parameter types and return types. Use Python 3.10 union syntax for
optional return values, and annotate every function consistently with the
existing data structures and behavior.
In `@tests/integration/defs/disaggregated/disagg_test_utils.py`:
- Around line 325-348: Add type annotations to get_registered_worker_urls,
including an int parameter and tuple[list[str], list[str]] return type. Annotate
the nested _urls helper with its role_key parameter as str and its return type
as list[str], preserving the existing URL discovery behavior.
In `@tests/integration/defs/stress_test/disagg_cancel/harness.py`:
- Around line 1908-1910: Update the injector logging calls in
tests/integration/defs/stress_test/disagg_cancel/harness.py at lines 1908-1910,
1921-1926, 1931-1937, 1989-1996, and 1999-2003 to pass each dynamic message as
one preformatted f-string argument, covering respawn failures, missing
registration, health-wait, invalid-port, polling-failure, and
registration-timeout messages.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: c013471a-9171-47df-80ba-b058551950c7
📒 Files selected for processing (4)
tests/integration/defs/common.pytests/integration/defs/disaggregated/disagg_test_utils.pytests/integration/defs/disaggregated/test_workers.pytests/integration/defs/stress_test/disagg_cancel/harness.py
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
tests/integration/defs/common.py (1)
674-706: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd annotations to the new helper functions.
tests/integration/defs/common.py#L674-L706: annotateaddr_path,timeout,process, and thetuple[str, int]return value.tests/integration/defs/accuracy/test_disaggregated_serving.py#L221-L231: annotatecluster_uriasstrand the return value asNone.As per coding guidelines, “Annotate every function.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/defs/common.py` around lines 674 - 706, Annotate wait_for_reported_addr in tests/integration/defs/common.py: use appropriate types for addr_path, timeout, and optional process, and retain tuple[str, int] as the return type. Also annotate the helper at tests/integration/defs/accuracy/test_disaggregated_serving.py:221-231 with cluster_uri: str and a None return type.Source: Coding guidelines
tensorrt_llm/commands/serve.py (1)
361-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the broad cleanup handler.
Line 367 catches
BaseException. This also catchesKeyboardInterruptandSystemExit.Use
finallyfor temporary-file cleanup. This preserves cleanup without a broad exception handler.Proposed fix
try: with os.fdopen(fd, "w") as f: f.write(f"{host}:{port}\n") f.flush() os.fsync(f.fileno()) os.replace(tmp_path, report_addr) - except BaseException: + finally: with contextlib.suppress(OSError): os.unlink(tmp_path) - raise🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tensorrt_llm/commands/serve.py` around lines 361 - 370, Replace the BaseException handler surrounding the temporary report-file write in the serve flow with a finally block that suppresses OSError while unlinking tmp_path. Preserve the existing write, fsync, atomic os.replace, and exception propagation behavior while ensuring cleanup runs on every exit path.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tensorrt_llm/commands/serve.py`:
- Around line 362-366: Update the report-address formatting around the
os.fdopen/os.replace block to wrap IPv6 host literals in brackets before
appending the port, while leaving IPv4 and hostname formatting unchanged. Ensure
a host such as ::1 is persisted as [::1]:<port> so consumers can construct valid
URL authorities.
---
Nitpick comments:
In `@tensorrt_llm/commands/serve.py`:
- Around line 361-370: Replace the BaseException handler surrounding the
temporary report-file write in the serve flow with a finally block that
suppresses OSError while unlinking tmp_path. Preserve the existing write, fsync,
atomic os.replace, and exception propagation behavior while ensuring cleanup
runs on every exit path.
In `@tests/integration/defs/common.py`:
- Around line 674-706: Annotate wait_for_reported_addr in
tests/integration/defs/common.py: use appropriate types for addr_path, timeout,
and optional process, and retain tuple[str, int] as the return type. Also
annotate the helper at
tests/integration/defs/accuracy/test_disaggregated_serving.py:221-231 with
cluster_uri: str and a None return type.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 681d153c-539d-4908-9bf8-21b308d67943
📒 Files selected for processing (5)
tensorrt_llm/commands/serve.pytests/integration/defs/accuracy/test_disaggregated_serving.pytests/integration/defs/common.pytests/integration/defs/perf/test_perf_sanity.pytests/unittest/api_stability/references/trtllm_serve_cli.yaml
42bc8fd to
8d8e38b
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
/bot run |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
tests/integration/defs/stress_test/disagg_cancel/harness.py (1)
1893-1910: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNarrow the exception handler around
_run_worker.Line 1908 catches
Exception._run_workeropens a config file, opens a log file, and callssubprocess.Popen, so the expected failures areOSErrorandyaml.YAMLError. A broad handler also swallows programming errors such asTypeErrorfrom a signature change and reports them as a respawn failure.♻️ Proposed change
- except Exception: + except (OSError, yaml.YAMLError): logger.exception("[injector] failed to respawn %s_%d", spec.role, spec.index) return FalseAs per coding guidelines: "Catch specific exceptions instead of using broad or bare
except:handlers."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/defs/stress_test/disagg_cancel/harness.py` around lines 1893 - 1910, In the respawn block around `_run_worker`, replace the broad `except Exception` handler with handling only the expected `OSError` and `yaml.YAMLError` failures. Keep the existing logger and `False` return for those exceptions, while allowing programming errors such as `TypeError` to propagate.Source: Coding guidelines
tests/integration/defs/common.py (1)
674-706: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd type annotations to the new helpers.
The repository guidelines require an annotation on every function.
wait_for_reported_addr,get_ephemeral_port_range,get_static_port_range, andreserve_port_from_rangeare new and unannotated.revise_disaggregated_server_config_urls_with_free_portsin the same module is already annotated, so the annotated style is established here.♻️ Proposed annotations
-def wait_for_reported_addr(addr_path, timeout, process=None): +def wait_for_reported_addr( + addr_path: str, + timeout: float, + process: subprocess.Popen | None = None, +) -> tuple[str, int]:-def get_ephemeral_port_range(): +def get_ephemeral_port_range() -> tuple[int, int] | None:-def get_static_port_range(): +def get_static_port_range() -> tuple[int, int] | None:-def reserve_port_from_range(port_range, source): +def reserve_port_from_range(port_range: tuple[int, int], source: str) -> int | None:As per coding guidelines: "Annotate every function, use
Nonefor procedures".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/defs/common.py` around lines 674 - 706, Add type annotations to wait_for_reported_addr, get_ephemeral_port_range, get_static_port_range, and reserve_port_from_range, covering every parameter and each return type; use None for procedures and preserve the established annotated style of revise_disaggregated_server_config_urls_with_free_ports.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/integration/defs/perf/test_perf_sanity.py`:
- Around line 1513-1518: Before launching the DISAGG_SERVER command in the
surrounding setup flow, remove the stale address file returned by
_disagg_server_addr_file(server_idx), matching the aggregated path’s cleanup
behavior. Keep the existing --report_addr argument and ensure cleanup occurs for
every retry before the new server publishes its resolved address.
---
Nitpick comments:
In `@tests/integration/defs/common.py`:
- Around line 674-706: Add type annotations to wait_for_reported_addr,
get_ephemeral_port_range, get_static_port_range, and reserve_port_from_range,
covering every parameter and each return type; use None for procedures and
preserve the established annotated style of
revise_disaggregated_server_config_urls_with_free_ports.
In `@tests/integration/defs/stress_test/disagg_cancel/harness.py`:
- Around line 1893-1910: In the respawn block around `_run_worker`, replace the
broad `except Exception` handler with handling only the expected `OSError` and
`yaml.YAMLError` failures. Keep the existing logger and `False` return for those
exceptions, while allowing programming errors such as `TypeError` to propagate.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 8facfe86-7122-42f4-9a17-457eded4e5b3
📒 Files selected for processing (8)
tensorrt_llm/commands/serve.pytests/integration/defs/accuracy/test_disaggregated_serving.pytests/integration/defs/common.pytests/integration/defs/disaggregated/disagg_test_utils.pytests/integration/defs/disaggregated/test_workers.pytests/integration/defs/perf/test_perf_sanity.pytests/integration/defs/stress_test/disagg_cancel/harness.pytests/unittest/api_stability/references/trtllm_serve_cli.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
- tests/integration/defs/disaggregated/test_workers.py
- tests/unittest/api_stability/references/trtllm_serve_cli.yaml
- tensorrt_llm/commands/serve.py
|
PR_Github #65002 [ run ] triggered by Bot. Commit: |
|
PR_Github #65002 [ run ] completed with state
|
brnguyen2
left a comment
There was a problem hiding this comment.
Approving — the comments below are optional touch-ups, not blockers.
The two-cause separation (TOCTOU race vs TIME_WAIT) with the measured SO_REUSEADDR matrix is exactly the level of analysis these flaky-port bugs needed, and I verified the key claims in-tree (HttpClusterStorageServer.__init__ ignoring the URI, pid-embedded worker_id matching, no index-sensitive consumers of the now-sorted worker URLs).
One gap worth closing before merge (inline): the num_serve_frontends > 1 path has the same port-0 hazard the disagg fleet rejection guards against, but is not rejected.
Smaller items:
- No unit tests for the new protocol.
_publish_bound_address+wait_for_reported_addrround-trip (including the IPv6-bracket and crashed-process paths) and the twoclick.BadParameterrejections are all cheaply testable withtmp_pathandCliRunner; right now the only coverage is indirect via GPU integration tests. - The PR description is cut off mid-sentence in section 3 ("The step id is de") — worth fixing since the rest of it is a useful reference for the next person who hits an
Errno 98in CI.
fredricz-20070104
left a comment
There was a problem hiding this comment.
Review summary - CONCERNS
Verdict: The two-cause analysis (reservation TOCTOU vs TIME_WAIT) is correct and the --port 0 + --report_addr + SO_REUSEADDR design fixes the targeted bug sites by construction, but there is one unhandled combination and no unit-level coverage of the new product code, so I'd close those before merge.
Concerns
-
[MAJOR]
tensorrt_llm/commands/serve.py:~1602---report_addrnot rejected for multi-frontend- What is wrong:
serve()rejects--report_addronly for the gRPC and VisualGen servers, anddisaggregated()rejects the SO_REUSEPORT fleet. Butlaunch_server's multi-frontend path (each frontend binds its own socket on the same port) has the identical port-0 hazard the fleet guard exists for. - How it fails: run
trtllm-serve ... --port 0 --report_addr fwithnum_serve_frontends > 1; each frontend child gets a different kernel-assigned port,_publish_bound_addresspublishes only the launcher socket's port, and a reader connecting to it hits a port that serves at best 1/N of requests → intermittent connection failures. - Suggested fix: mirror the disagg-fleet guard — reject
--port 0/--report_addrwhen more than one frontend is configured, or only publish once a single shared socket is guaranteed inherited by all frontends.
- What is wrong:
-
[MAJOR]
tensorrt_llm/commands/serve.py:641- new publisher/rejection logic has no unit test- What is wrong:
_publish_bound_address, the SO_REUSEADDR sites,wait_for_reported_addr, and the twoclick.BadParameterrejections are only exercised indirectly through GPU integration tests, which the PR says were not run locally. - How it fails: a regression in the publish/parse round-trip (IPv6 bracketing, dead-process fast-fail, timeout) or a mis-scoped rejection ships untested.
- Suggested fix: add a
tmp_path+CliRunnerunit test covering the publish/read round-trip (IPv4, IPv6, crashed process, timeout) and the grpc/visual_gen/fleet rejections.
- What is wrong:
Minor notes (non-blocking)
tensorrt_llm/commands/serve.py:367-except BaseExceptionfor temp-file cleanup; afinally:expresses the intent without catching KeyboardInterrupt/SystemExit.tests/integration/defs/stress_test/disagg_cancel/harness.py:1908- broadexcept Exceptionaround_run_workeralso masks aTypeErrorfrom the newworker_index=arg; narrow to(OSError, yaml.YAMLError).tests/integration/defs/disaggregated/disagg_test_utils.py:326and the newcommon.pyhelpers lack type annotations required by the repo guidelines.
QA view
- Test coverage: partial - integration migrations touch the changed paths but need GPUs and were not run locally; no unit test covers the publisher, reader, or the two rejection branches.
- SM coverage: architecture-independent - pure socket/port/file coordination, no arch guards or fp8/nvfp4 paths.
- Test code: missing annotations; broad
except Exceptionin the respawn path; migrations unverified locally. - Test time: unknown - polling loops (
sleep(0.5)/sleep(10)) replace fixed-port logic; no new parametrisation or model, so no clear signal from the diff. - Needs
/qa-verify: yes - intermittent-race fix with no reproducing test, integration migrations not run locally, and changes to perf_sanity SLURM coordination + disagg_cancel respawn infra that only CI can exercise.
Does this actually fix nvbugs/6435121, 6567057, 6526529?
Partial. The mechanism is correct: --port 0 keeps the socket bound from kernel assignment through uvicorn takeover (killing the reservation TOCTOU), and SO_REUSEADDR on all three bind sites addresses the TIME_WAIT rebind failure the 6435121 diagnostic showed. This is applied to exactly the sites the three bugs land on. But there is no test that reproduces the original intermittent failure, the migrations were not run locally, and other pre-picking sites are explicitly left racing (now from a safer range). Fixed by construction, unverified in practice.
Possible new issues
- If
launch_serverdoes not reassignport = s.getsockname()[1]after binding with port 0 (the diff shows this only in thedisaggregatedcommand, not inlaunch_server),_publish_bound_addresswould publish0. Please confirm the resolution happens before the publish call. - perf_sanity now reads address files written by trtllm-serve with a trailing newline (
host:port\n), where the old_generate_hostname_filewrote none; confirm the file-read in_generate_disagg_server_configstrips whitespace before parsing the port.
What I could not verify
- The elided
launch_serverbinding region between the two hunks — specifically whetherportis resolved fromgetsockname()before_publish_bound_address. - Whether the default
num_serve_frontendsis 1 (making the multi-frontend gap opt-in) or greater (making it a common-path defect). - The whitespace-stripping in perf_sanity's hostname-file read (outside the shown hunk).
Automated review by NVCortex Lite, run by @fredricz-20070104.
fredricz-20070104
left a comment
There was a problem hiding this comment.
Review summary - Approve (non-blocking)
Approving so this is not blocked on me. The points raised in my review comment above are non-blocking — please read them and address what you agree with before merging.
Worth doing before this is relied on: It is a fix for an intermittent port race with no test that reproduces the original failure, the integration migrations were not run locally, and it touches test infrastructure (perf_sanity SLURM coordination, disagg_cancel respawn) plus multi-node coordination paths that only CI can exercise.
Automated review by NVCortex Lite, run by @fredricz-20070104.
|
Rebased onto main to pick up the infrastructure fix. Re-running with fail-fast disabled. |
|
/bot run --disable-fail-fast |
|
PR_Github #65869 [ run ] triggered by Bot. Commit: |
|
PR_Github #65866 [ run ] completed with state |
… the CI port allocator Several disagg CI failures share one mechanism: the test harness pre-picks a port with get_free_port(), which binds, reads getsockname() and then closes the socket, and hands the number to a trtllm-serve subprocess that binds it much later. Anything can take the port in between. 82c1ba8 addressed this for test_auto_scaling by passing --port 0 and letting service discovery report the address the worker actually bound. Apply the same method to the remaining call sites where service discovery is already configured, and close the allocator hole that made the race reachable at all. - test_workers.py::background_workers configured a full disagg_cluster and then still handed --port N to every ctx/gen worker. Launch with port=0 and read the real URLs back from the cluster registry once the server reports ready. The URL format is unchanged: a worker whose host and cluster_uri are both localhost registers as localhost, so the router/tester call sites are unaffected. Also pass worker_index, which the function omitted. - disagg_cancel/harness.py pre-picked a port when relaunching a SIGKILLed worker, while the initial launch already used port=0. Resolving the port needs a lookup, since new_wrapper.port feeds the /health poll, so match the registry entry on pid: WorkerInfo.worker_id embeds os.getpid() of the trtllm-serve process. Matching on "a port we have not seen before" would be ambiguous while the killed worker's stale registration is still being reaped. Registration and health now share one deadline instead of each getting the full timeout. Also pass worker_index, whose absence made a respawn of worker N truncate worker 0's log out from under the log scanner. - get_free_port_in_ci fell straight through to get_free_port() when CONTAINER_PORT_START is unset, i.e. the SLURM multi-node path, drawing reserved ports from the very ephemeral pool that trtllm-serve's own --port 0 workers bind from. Add an intermediate fallback that reserves from a window just below /proc/sys/net/ipv4/ip_local_port_range, which bind(('', 0)) never hands out, so a reserved port can no longer be taken by a sibling worker. The existing probe-bind loop is extracted into reserve_port_from_range() and shared by both ranges; the ephemeral fallback remains as a last resort. Partially addresses https://nvbugs/6567057, https://nvbugs/6435121 and https://nvbugs/6526529. The front ports those bugs fail on (the disagg server and perf-sanity worker ports) still pre-pick, now from a safer range; closing them needs trtllm-serve to publish its resolved bind address. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…se them in disagg tests Two independent causes hide behind the same EADDRINUSE: 1. TIME_WAIT tombstones. launch_server and the disaggregated server bound their sockets without SO_REUSEADDR, so after a server exits, the TIME_WAIT entries of the connections it accepted refuse a rebind of that port for ~60s. This is not a race and no amount of port juggling avoids it. Measured: the flag has to be set on the socket that owned the port first, because the TIME_WAIT entry inherits it -- setting it only on the later bind is not enough. Set it unconditionally on all three HTTP bind sites. The main beneficiary is the product path, where users pass an explicit --port and restart. 2. Reserving a port before the process that binds it exists. A harness picks a port, closes the probe socket, and hands the number to a trtllm-serve that binds it much later; anything can take it in between. For (2), add --report_addr: with --port 0 the kernel assigns the port, the socket stays bound from that moment until uvicorn takes it over, and the resolved host:port is published atomically (temp file + rename, so a reader never sees a partial line -- it matters on the shared filesystems multi-node tests coordinate through). This is the same shape the KV cache transceiver already uses, where the ZMQ rendezvous socket binds ":*" and its address rides out in-band; that path has never produced a port conflict. Reservation is inherently host-local, but publication is not, which is why this works multi-node: every site that picks a port does so on the node that will bind it, and only the resolved address has to travel. --report_addr is rejected for the gRPC and VisualGen servers, and for the disagg fleet topologies, rather than silently never being written: with num_workers>1 the port goes to N SO_REUSEPORT workers, which under port 0 would each get a different kernel-assigned port instead of sharing one. test_disaggregated_serving.py now starts the disaggregated server first with --port 0, reads back the address, and only then writes the worker configs carrying the resolved cluster_uri. The server's own copy of cluster_uri keeps a placeholder port because HttpClusterStorageServer serves the storage on the server's own port and never reads the URI; only workers dial it. Addresses https://nvbugs/6567057 and https://nvbugs/6435121. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…of reserving one The aggregated server, the disaggregated server and the CTX/GEN workers each picked a port with get_free_port() and handed the number to a trtllm-serve that bound it much later. On a 44-GPU/11-node stage that window is wide, and https://nvbugs/6526529 caught the GEN server losing its port in exactly that gap. Launch all three with --port 0 --report_addr instead: the kernel assigns the port, the socket stays bound from that moment, and the server publishes the resolved host:port itself. The cross-node coordination channel is unchanged in shape -- the CTX/GEN tasks still deposit host:port files that the DISAGG_SERVER task turns into its config -- except those files are now written by the servers rather than guessed by the harness. Reservation stays host-local, which is what makes this work multi-node: every task picks a port for a server on its own node, and only the resolved address crosses nodes. Two things this needs to be correct: - The coordination directory is now scoped by SLURM_JOB_ID. test_output_dir is derived from the test case name alone and created with exist_ok=True, so a rerun of the same case reused it; once the files are server-written rather than harness-written, a leftover file from a previous run would point the disagg server at a dead worker, which fails far less obviously than a port conflict. The step id is deliberately excluded, since each role is a separate srun step within one job and they must agree on the path. - The directory scan filters to *.txt. The address is published by renaming a "<name>.<rand>.tmp" sibling into place, and counting those transient entries would both inflate the expected-count check and get parsed as a worker url. The BENCHMARK task now waits on the disagg server's reported address rather than reading the port out of the generated config, which under port 0 would be 0. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…thority
Three findings from the PR review, all verified against the code first:
- disagg_cancel respawn leaked the new worker past teardown. _teardown_cluster
terminates the wrapper lists unpacked from self._cluster, not
self._tracked_workers, so a respawn that only updated tracked.wrapper stayed
alive after the test and kept holding its GPUs. Replace the slot in the
cluster list too, before the port wait, so a respawn that never registers is
cleaned up as well. spec.index is per-role, matching how the ctx/gen spec
lists are built. Pre-existing, but in the function this PR rewrote.
- get_ephemeral_port_range() accepted implausible /proc contents. With
"70000 80000" it yielded a static window of (65904, 69999), and bind() raises
OverflowError rather than OSError for ports above 65535, so
reserve_port_from_range would propagate it instead of trying another port.
Reject anything outside 1 <= low <= high <= 65535 and fall through, which is
what the docstring already claimed.
- The reported address was not a valid URL authority for IPv6. --host ::1 wrote
"::1:<port>", and consumers build "http://<reported>" verbatim. Bracket IPv6
literals so it reads "[::1]:<port>"; the reader's rpartition(":") keeps
working and now yields a host that is directly usable in a URL.
A fourth comment suggested replacing the asserts in get_registered_worker_urls
with explicit exceptions. Skipped: the "raise ValueError instead of assertions"
rule in CODING_GUIDELINES.md sits under the Pydantic validation section, the
neighbouring verify_cluster_info in the same file asserts the same way, and
these tests never run under -O.
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…parsing, unit tests
Follow-up on review feedback, all verified against the code first.
- launch_server did not reject --port 0 / --report_addr under
num_serve_frontends > 1, which has the hazard the disaggregated fleet guard
already covers: _spawn_attached_frontends re-execs this command line
verbatim, so every frontend binds its own kernel-assigned port instead of
sharing one, and every frontend re-runs _publish_bound_address, leaving the
reader with whichever child wrote last. Rejected the same way as the fleet.
- A wildcard bind host was published verbatim, so --host 0.0.0.0 (or ::) wrote
an address no reader can dial, even though the value is documented as a URL
authority used as-is. Substitute this machine's hostname.
- The respawn path matched the worker pid as the substring "-<pid>-" inside
worker_id, which is "{role}-{host}:{port}-{time_ms}-{pid}-{rand}". A host
name containing a dash-delimited digit run, such as node-1234-a, false-matches
another worker's pid. Parse the fixed tail instead, via _worker_id_pid.
- perf sanity removed the stale aggregated address file before launch but not
the disaggregated one. The coordination directory is scoped by job, so a new
job is safe, but a retry within the same job and the same server index would
point the benchmark task at the previous attempt's dead port.
- Added unit coverage for the new product code, which until now was only
exercised by GPU integration stages: the publish/read round trip (IPv4, IPv6
bracketing, wildcard substitution, missing parent directories, no-op without
a path), atomicity (no leaked temp files, no partial line under a concurrent
reader), reader behaviour (dead process fast-fail, timeout, late write), and
both launch_server rejections. Registered in the CPU test list.
- Replaced except BaseException with finally for temp-file cleanup, and added
the missing type annotations on the new helpers.
Two review questions, both confirmed in the existing code rather than changed:
launch_server does reassign port from getsockname() before publishing, and
_generate_disagg_server_config already strips whitespace when reading the
address files.
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…leet guard Two review findings on the new unit test. The atomicity test could fail without any defect present: nothing stopped the publish loop from finishing and setting the stop event before the reader thread was ever scheduled, leaving the observed set empty. The reader now signals after its first read attempt and the publish loop waits for that signal. The disaggregated fleet guard had no coverage; only the multi-frontend guard in launch_server was exercised. Added a command-line test over the three rejected combinations. Everything the guard lets through goes on to bind a socket and serve, so only the rejected combinations are exercised. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…guard test The third parametrised case failed in CI with FileNotFoundError instead of the expected rejection. Cause is in set_prometheus_multiproc_dir, which disaggregated() calls before reaching the guard: the second call in a process rebinds the module global holding the TemporaryDirectory, which deletes the directory the environment variable still points at, so the third call tries to create a subdirectory inside a path that no longer exists. Production is unaffected because a server process calls that function once. Only a test that invokes the command repeatedly in one process reaches it, so the fix here is to point the variable at a directory that outlives every invocation rather than to change the shared helper. The other 21 cases in this file passed. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
a67e6f6 to
a28b00b
Compare
|
Rebased onto main again, now including the mypy fix from #17637. Re-running with fail-fast disabled. |
|
/bot run --disable-fail-fast |
|
PR_Github #65936 [ run ] triggered by Bot. Commit: |
|
PR_Github #65869 [ run ] completed with state |
|
PR_Github #65936 [ run ] completed with state
|
|
Status after the last run on The coverage this pull request was waiting for has now passed.
Those exercise the address publication path across two and three nodes: port 0 with Why the pipeline is still red: x86 single-GPU had 7 failures out of 53933, none in code this pull request touches.
x86 multi-GPU was then blocked by the rule that multi-GPU requires single-GPU to pass, which fail-fast being disabled does not bypass, so Re-running. |
|
/bot run --disable-fail-fast |
|
PR_Github #66600 [ run ] triggered by Bot. Commit: |
|
PR_Github #66600 [ run ] completed with state |
…es between Ray tests
A passing Ray disaggregated serving test can leave live trtllm-serve/orted subprocesses behind: the next test's servers then fail to bind ports 8000-8002 and the proxy never comes up ('Disaggregated server failed to start'). SO_REUSEADDR (added on main by NVIDIA#17460) only covers sockets left in TIME_WAIT, not ports held by live processes. Before and after each Ray disagg test, kill stale trtllm-serve/orted processes (only port owners and orphans, sparing unrelated jobs on shared machines and this pytest process's own MPI orted daemon) and wait until the disagg ports stop listening.
Also raise the test's wait_for_server timeout to 300s so it exceeds the proxy's server_start_timeout (180s) and the proxy's own error gets logged before teardown, and remove the waives for nvbugs 6601574/6601575.
Verified on 4xB200: tp2-CPP, tp2-PYTHON, tp1-CPP, tp1-PYTHON pass back-to-back with leftovers present at start.
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…es between Ray tests
A passing Ray disaggregated serving test can leave live trtllm-serve/orted subprocesses behind: the next test's servers then fail to bind ports 8000-8002 and the proxy never comes up ('Disaggregated server failed to start'). SO_REUSEADDR (added on main by NVIDIA#17460) only covers sockets left in TIME_WAIT, not ports held by live processes. Before and after each Ray disagg test, kill stale trtllm-serve/orted processes (only port owners and orphans, sparing unrelated jobs on shared machines and this pytest process's own MPI orted daemon) and wait until the disagg ports stop listening.
Also raise the test's wait_for_server timeout to 300s so it exceeds the proxy's server_start_timeout (180s) and the proxy's own error gets logged before teardown, and remove the waives for nvbugs 6601574/6601575.
Verified on 4xB200: tp2-CPP, tp2-PYTHON, tp1-CPP, tp1-PYTHON pass back-to-back with leftovers present at start.
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…es between Ray tests
A passing Ray disaggregated serving test can leave live trtllm-serve/orted subprocesses behind: the next test's servers then fail to bind ports 8000-8002 and the proxy never comes up ('Disaggregated server failed to start'). SO_REUSEADDR (added on main by NVIDIA#17460) only covers sockets left in TIME_WAIT, not ports held by live processes. Before and after each Ray disagg test, kill stale trtllm-serve/orted processes (only port owners and orphans, sparing unrelated jobs on shared machines and this pytest process's own MPI orted daemon) and wait until the disagg ports stop listening.
Also raise the test's wait_for_server timeout to 300s so it exceeds the proxy's server_start_timeout (180s) and the proxy's own error gets logged before teardown, and remove the waives for nvbugs 6601574/6601575.
Verified on 4xB200: tp2-CPP, tp2-PYTHON, tp1-CPP, tp1-PYTHON pass back-to-back with leftovers present at start.
Signed-off-by: Chuang Zhu <111838961+chuangz0@users.noreply.github.com>
…tion race with --port 0 + --report_addr (NVIDIA#17460) Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
Summary
Three CI bugs share one shape: a harness picks a port with
get_free_port(), which binds a probe socket, closes it, and hands the number to atrtllm-servethat binds it much later. Anything can take the port in between — including the test's own sibling workers launched with--port 0, drawing from the same ephemeral pool.This PR closes that race for the sites those bugs land on, and fixes a second, unrelated cause hiding behind the same
EADDRINUSE.1.
SO_REUSEADDR— a different bug with the same error messagelaunch_serverand the disaggregated server bound their sockets withoutSO_REUSEADDR, so after a server exits, the TIME_WAIT tombstones of the connections it accepted refuse a rebind of that port for ~60s. This is not a race, and no amount of port juggling avoids it. It is why nvbugs/6435121's diagnostic reads127.0.0.1:10879 status=TIME_WAIT.Measured matrix — the flag must be set on the socket that owned the port first, because the TIME_WAIT entry inherits it:
Set unconditionally on all three HTTP bind sites. The main beneficiary is the product path, where users pass an explicit
--portand restart —--port 0is immune to TIME_WAIT anyway (verified: 20000bind(0)calls with 61 TIME_WAIT ports present, zero failures).2.
--report_addr— eliminating the reservation windowWith
--port 0the kernel assigns the port, the socket stays bound from that moment until uvicorn takes it over, and the resolvedhost:portis published atomically (temp file +rename, so a reader never sees a partial line — that matters on the shared filesystems multi-node tests coordinate through).This is the same shape the KV cache transceiver already uses:
ucxCacheCommunicator.cpp:352bindstcp://<ip>:*, reads the port back fromZMQ_LAST_ENDPOINT, and ships it to the peer in-band viaCommState→opaque_state. That path has never produced a port conflict, because no port in it is chosen by a process other than the one that binds it.Why this works multi-node: reservation is inherently host-local, but publication is not. Every site that picks a port does so on the node that will bind it — including perf sanity, where each SLURM task launches its own local server — so only the resolved address has to cross nodes.
--report_addris rejected for the gRPC and VisualGen servers and for the disagg fleet topologies rather than silently never being written: withnum_workers>1the port goes to NSO_REUSEPORTworkers, which under port 0 would each get a different kernel-assigned port instead of sharing one.3. Migrated sites
test_disaggregated_serving.py(nvbugs/6567057, nvbugs/6435121) — starts the disagg server first with--port 0, reads back the address, and only then writes the worker configs carrying the resolvedcluster_uri. The server's own copy ofcluster_urikeeps a placeholder port becauseHttpClusterStorageServer.__init__serves the storage on the server's own port and never reads the URI; only workers dial it.test_perf_sanity.py(nvbugs/6526529) — all three sites (aggregated server, disagg server, CTX/GEN workers) now use--port 0 --report_addr. The cross-node channel is unchanged in shape; thosehost:portfiles are now written by the servers rather than guessed by the harness. Two prerequisites this needed:SLURM_JOB_ID.test_output_diris derived from the test case name alone and created withexist_ok=True, so a rerun reused it — once files are server-written, a leftover from a previous run points at a dead worker, which fails far less obviously than a port conflict. The step id is deliberately excluded, since each role is a separate srun step within one job.*.txt, or the transient.tmprename siblings would inflate the expected-count check and get parsed as worker urls.Earlier commit (test-only) —
test_workers.py::background_workersand thedisagg_cancelrespawn path now launch workers withport=0and read URLs back from/cluster_info;get_free_port_in_cigained a fallback below the ephemeral range for the SLURM path, whereCONTAINER_PORT_STARTis never set (jenkins/L0_Test.groovy:1200is the only setter, and it is the single-node container path).Scope — what is not fixed
Other sites still pre-pick, now only from a safer range:
test_disaggregated.py:683,694,test_workers.py:571,disagg_test_utils.py:426,test_ad_disagg_trtllm_serve.py:184,test_dwdp_disaggregated_serving.py, andRemoteOpenAIServer(tests/unittest/llmapi/apps/openai_server.py:35, 38 dependent files).Separately, the
MASTER_PORT/ c10d TCPStore sites cannot use--port 0as written, but are fixable by the same pattern — rank 0 createsTCPStore(host, 0, ...), which binds and holds, then broadcastsstore.portover thempi_broadcast/pipe channel these sites already have. VerifiedTCPStoreexposes.porton the pinned torch. Follow-up.Test Coverage
Product change is small and mechanical; the risk sits in the test migrations, which need GPUs and have not been run locally — CI is the real verification.
Verified directly here:
nvl72d066-T01), IPv4, fast-fail when the server process dies, and timeout.CTX_*,GEN_*,DISAGG_SERVER,BENCHMARK) agree on the coordination dir withSLURM_STEP_IDset differently per role; a differentSLURM_JOB_IDyields a different dir; the.txtfilter excludes the disagg.addrfile.SO_REUSEADDRandbind(0)-vs-TIME_WAIT matrices above.pre-commitpasses on all touched files;trtllm_serve_cli.yamlupdated for both subcommands.Affected CI:
disaggregated/test_workers.py→A10-PyTorch-{1,2,3},DGX_B200-PyTorch-*,DGX_H100-*-PyTorch-Others-*; plusaccuracy/test_disaggregated_serving.pyand the perf-sanity disagg stages.PR Checklist
[JIRA/NVBUG/None][type] Summary🤖 Generated with Claude Code
Dev Engineer Review
--report_addrpublication andSO_REUSEADDR.--port 0and--report_addrtopologies.MASTER_PORT/TCPStore usage remain out of scope.QA Engineer Review
tests/unittest/llmapi/test_serve_report_addr.py.launch_servervalidation.unittest/llmapi/test_serve_report_addr.pytotests/integration/test_lists/test-db/l0_cpu.yml.