Skip to content

(feat) Support multi-node aggregate jobs with direct vllm frontend - #296

Merged
kyleliang-nv merged 10 commits into
NVIDIA:mainfrom
arpera:vllm-forntend-for-multinode
Aug 13, 2026
Merged

(feat) Support multi-node aggregate jobs with direct vllm frontend#296
kyleliang-nv merged 10 commits into
NVIDIA:mainfrom
arpera:vllm-forntend-for-multinode

Conversation

@arpera

@arpera arpera commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Add vllm frontend support for aggregated multi-node vllm jobs. Initial work on adding support vllm frontend for aggregated single-node vllm jobs has been done in #278. Then in srt-slurm fork @functionstackx added support for multi-node case functionstackx#1 (which is the base for this PR but it has been extended a lot; also improves docs).

This vllm frontend support is required to make reproducible Kimi-K3 multi-node recipes uploaded to SA here SemiAnalysisAI/InferenceX#2391

@functionstackx

Copy link
Copy Markdown

hi @arpera thanks for upstreaming this & improving upon it!

@kyleliang-nv
kyleliang-nv force-pushed the vllm-forntend-for-multinode branch from 18c1866 to a7d57bc Compare August 11, 2026 08:20
arpera and others added 7 commits August 11, 2026 06:14
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com>
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com>
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com>
Signed-off-by: Kyle Liang <kylliang@nvidia.com>
Signed-off-by: Kyle Liang <kylliang@nvidia.com>
Signed-off-by: Kyle Liang <kylliang@nvidia.com>
Signed-off-by: Kyle Liang <kylliang@nvidia.com>
@kyleliang-nv
kyleliang-nv force-pushed the vllm-forntend-for-multinode branch from a7d57bc to 9a74c2f Compare August 11, 2026 13:16
@codecov-commenter

codecov-commenter commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.33962% with 6 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@50cc900). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/srtctl/frontends/vllm.py 0.00% 3 Missing ⚠️
src/srtctl/backends/vllm.py 98.36% 1 Missing ⚠️
src/srtctl/cli/mixins/benchmark_stage.py 92.30% 1 Missing ⚠️
src/srtctl/core/schema.py 91.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #296   +/-   ##
=======================================
  Coverage        ?   72.61%           
=======================================
  Files           ?       77           
  Lines           ?    10698           
  Branches        ?        0           
=======================================
  Hits            ?     7768           
  Misses          ?     2930           
  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.

Comment thread src/srtctl/backends/vllm.py Outdated
Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com>
arpera and others added 2 commits August 12, 2026 22:34
…tinode

Signed-off-by: Artem Perevedentsev <aperevedents@nvidia.com>
Signed-off-by: Kyle Liang <kylliang@nvidia.com>
@kyleliang-nv
kyleliang-nv merged commit e32e591 into NVIDIA:main Aug 13, 2026
6 checks passed
nv-yna added a commit to nv-yna/srt-slurm that referenced this pull request Aug 14, 2026
main's e32e591 (direct vLLM frontend, NVIDIA#296) split _public_api_node out of
_orchestrator_node: for a single-worker direct-vLLM aggregated job the agg
leader serves FRONTEND_PUBLIC_PORT, not the orchestrator. This branch was
written before that split and paired the orchestrator node with that port,
so on such a job the scraper would poll a node that is not listening and
the run would come back with no frontend rows at all.

Every other FRONTEND_PUBLIC_PORT pairing in the file already uses
_public_api_node; this makes the scrape target agree. No behaviour change on
the Dynamo path, where _public_api_node delegates to _orchestrator_node.

Test asserts the frontend target lands on the agg leader and pins that the
orchestrator would have been the wrong answer, so the two cannot silently
converge again.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
nv-yna added a commit to nv-yna/srt-slurm that referenced this pull request Aug 14, 2026
Worker targets are the leaders' DYN_SYSTEM_PORT, which only Dynamo serves.
main's NVIDIA#296 made the divergence explicit: _logical_worker_endpoints now
selects sys_port / frontend_port / http_port by frontend.type, while this
capture always assumed the first.

Scoping rather than generalising, because the scope was already there
implicitly -- expand_observability sets publish_events_and_metrics and the
DYN_LOGGING_* span env, none of which a non-Dynamo frontend consumes, so
such a run has no worker surface for this to find. Say so in the docstring
and point at _logical_worker_endpoints for the mapping if it ever needs one.

A non-Dynamo run now returns the frontend target with a warning instead of
worker URLs nothing is listening on. Emitting them would have produced a run
that looks instrumented and comes back with no worker rows -- the exact
failure this knob exists to prevent -- with only a rate-limited scraper
warning to explain it.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
nv-yna added a commit to nv-yna/srt-slurm that referenced this pull request Aug 17, 2026
main's e32e591 (direct vLLM frontend, NVIDIA#296) split _public_api_node out of
_orchestrator_node: for a single-worker direct-vLLM aggregated job the agg
leader serves FRONTEND_PUBLIC_PORT, not the orchestrator. This branch was
written before that split and paired the orchestrator node with that port,
so on such a job the scraper would poll a node that is not listening and
the run would come back with no frontend rows at all.

Every other FRONTEND_PUBLIC_PORT pairing in the file already uses
_public_api_node; this makes the scrape target agree. No behaviour change on
the Dynamo path, where _public_api_node delegates to _orchestrator_node.

Test asserts the frontend target lands on the agg leader and pins that the
orchestrator would have been the wrong answer, so the two cannot silently
converge again.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
nv-yna added a commit to nv-yna/srt-slurm that referenced this pull request Aug 17, 2026
Worker targets are the leaders' DYN_SYSTEM_PORT, which only Dynamo serves.
main's NVIDIA#296 made the divergence explicit: _logical_worker_endpoints now
selects sys_port / frontend_port / http_port by frontend.type, while this
capture always assumed the first.

Scoping rather than generalising, because the scope was already there
implicitly -- expand_observability sets publish_events_and_metrics and the
DYN_LOGGING_* span env, none of which a non-Dynamo frontend consumes, so
such a run has no worker surface for this to find. Say so in the docstring
and point at _logical_worker_endpoints for the mapping if it ever needs one.

A non-Dynamo run now returns the frontend target with a warning instead of
worker URLs nothing is listening on. Emitting them would have produced a run
that looks instrumented and comes back with no worker rows -- the exact
failure this knob exists to prevent -- with only a rate-limited scraper
warning to explain it.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
nv-yna added a commit that referenced this pull request Aug 17, 2026
* feat(observability): in-job RAW /metrics scraper

Worker and frontend /metrics endpoints are served by the live processes.
When the SLURM job ends they disappear and the data is gone -- there is
no after-the-fact scrape. Anything wanting a time series of KV-cache
occupancy, router queue depth or frontend event-loop pressure has to
capture it DURING the run.

Adds a daemon thread, started with the benchmark stage and gated on
observability.scrape_metrics (which defaults to observability.enabled),
that appends each response body VERBATIM to raw_prometheus.jsonl:

    {"timestamp_ns":..., "endpoint_url":..., "role":..., "worker_id":...,
     "text": "<raw Prometheus exposition, UNPARSED>"}

Capture emits RAW, the processor parses -- so a parser fix never requires
re-running the job. timestamp_ns is wall-epoch, aligning with client
records and spans on the same clock.

Best-effort by construction: an unreachable endpoint costs one line and
a rate-limited warning, never the benchmark. It stops on its own signal
as well as the shared stop_event, because the latter only fires on abort
-- relying on it alone would leave the scraper polling endpoints that are
being torn down.

Targets are the frontend plus every worker LEADER; follower ranks would
duplicate rows under a misleading worker_id.

Verified on Lyris jobs 2674508 and 2674375: 7625 lines across all three
roles, worker_id set on 6100/6100 worker rows, trtllm_kv_cache_* present
in the raw text, 4572 s scrape window.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>

* fix(observability): gate RAW scrape-target resolution on the opt-in

try_start_raw_scraper checks observability.scraper_enabled internally, but
the target list was built inside its argument list, so Python evaluated it
before the guard could ever run. Two consequences:

- _analytics_scrape_targets() reaches self.backend_processes, which
  BenchmarkStageMixin declares as a NotImplementedError seam. Any caller
  that does not implement it now crashes in _run_benchmark_script --
  including the TestBenchmarkChildReaping harness on main, which is what
  turned CI red.
- Every target costs a get_hostname_ip() srun round-trip inside a Slurm
  job. That was paid on each benchmark run even with the scraper switched
  off, and it sat outside the callee's best-effort try/except, so a
  resolution failure was fatal to a run that never wanted the scraper.

Check the opt-in at the call site so targets are resolved only when the
scraper actually starts. The check is `is True` rather than plain
truthiness on purpose: scraper_enabled is a bool property, but this mixin
is routinely driven with a mocked config whose every attribute is truthy,
and truthiness would silently switch the scraper on for those callers.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>

* style: apply ruff format to test_metrics_scraper

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>

* fix(observability): scrape the frontend on the public API node

main's e32e591 (direct vLLM frontend, #296) split _public_api_node out of
_orchestrator_node: for a single-worker direct-vLLM aggregated job the agg
leader serves FRONTEND_PUBLIC_PORT, not the orchestrator. This branch was
written before that split and paired the orchestrator node with that port,
so on such a job the scraper would poll a node that is not listening and
the run would come back with no frontend rows at all.

Every other FRONTEND_PUBLIC_PORT pairing in the file already uses
_public_api_node; this makes the scrape target agree. No behaviour change on
the Dynamo path, where _public_api_node delegates to _orchestrator_node.

Test asserts the frontend target lands on the agg leader and pins that the
orchestrator would have been the wrong answer, so the two cannot silently
converge again.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>

* docs(observability): declare the RAW worker capture Dynamo-scoped

Worker targets are the leaders' DYN_SYSTEM_PORT, which only Dynamo serves.
main's #296 made the divergence explicit: _logical_worker_endpoints now
selects sys_port / frontend_port / http_port by frontend.type, while this
capture always assumed the first.

Scoping rather than generalising, because the scope was already there
implicitly -- expand_observability sets publish_events_and_metrics and the
DYN_LOGGING_* span env, none of which a non-Dynamo frontend consumes, so
such a run has no worker surface for this to find. Say so in the docstring
and point at _logical_worker_endpoints for the mapping if it ever needs one.

A non-Dynamo run now returns the frontend target with a warning instead of
worker URLs nothing is listening on. Emitting them would have produced a run
that looks instrumented and comes back with no worker rows -- the exact
failure this knob exists to prevent -- with only a rate-limited scraper
warning to explain it.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>

* feat(observability): default the scrape interval to 1s

3s was a conservative first guess. The panels this feeds are per-request
timescale -- KV-cache occupancy and queue depth move within a single
decode burst -- so a 3s cadence aliases exactly the transients the capture
exists to show.

Changed in all three places the default is spelled: the schema field, the
RawMetricsScraper constructor, and try_start_raw_scraper's getattr fallback
for configs that omit the field. A test pins them together so they cannot
drift apart again.

Cost is bounded by construction: the 0.5s floor still applies, and the
drift-free pacing means a sweep slower than the interval runs back-to-back
rather than queueing. Volume scales with it -- the 76-minute Lyris run
produced 7625 lines at 3s, so expect roughly 3x that at 1s.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>

---------

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
Co-authored-by: Yuewei Na <nv-yna@users.noreply.github.com>
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.

5 participants